diff --git a/hoshino/config_example/clanbattle.py b/hoshino/config_example/clanbattle.py new file mode 100644 index 000000000..2d9b7c8ff --- /dev/null +++ b/hoshino/config_example/clanbattle.py @@ -0,0 +1,25 @@ +class JP: + BOSS_HP = [ + [6000000, 8000000, 10000000, 12000000, 15000000], + [6000000, 8000000, 10000000, 12000000, 15000000], + [7000000, 9000000, 13000000, 15000000, 20000000], + [15000000, 16000000, 18000000, 19000000, 20000000], + ] + + +class TW: + BOSS_HP = [ + [6000000, 8000000, 10000000, 12000000, 15000000], + [6000000, 8000000, 10000000, 12000000, 15000000], + [7000000, 9000000, 13000000, 15000000, 20000000], + [15000000, 16000000, 18000000, 19000000, 20000000], + ] + + +class BL: + BOSS_HP = [ + [6000000, 8000000, 10000000, 12000000, 20000000], + [6000000, 8000000, 10000000, 12000000, 20000000], + [6000000, 8000000, 10000000, 12000000, 20000000], + [6000000, 8000000, 10000000, 12000000, 20000000], + ] diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/api.py b/hoshino/modules/pcrclanbattle/clanbattlev3/api/__init__.py similarity index 100% rename from hoshino/modules/pcrclanbattle/clanbattlev3/api.py rename to hoshino/modules/pcrclanbattle/clanbattlev3/api/__init__.py diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/argparser.py b/hoshino/modules/pcrclanbattle/clanbattlev3/argparser.py new file mode 100644 index 000000000..55917d8b1 --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/argparser.py @@ -0,0 +1,52 @@ +from typing import Dict, List + +from .exception import ParseError + + +class ArgHolder: + __slots__ = ("type", "default", "tip") + + def __init__(self, tip, type_=str, default=None): + self.type = type_ + self.default = default + self.tip = tip + + +class ArgParser: + def __init__(self, arg_dict=None): + self.arg_dict: Dict[str, ArgHolder] = arg_dict or {} + + def add_arg(self, prompt, tip, type_=str, default=None): + self.arg_dict[prompt] = ArgHolder(tip, type_, default) + + def parse(self, args: List[str]) -> dict: + result = {} + + # 解析参数,以一个字符开头,或无前缀 + for arg in args: + prompt, x = arg[0].upper(), arg[1:] + if prompt in self.arg_dict: + holder = self.arg_dict[prompt] + elif "" in self.arg_dict: + holder = self.arg_dict[""] + prompt, x = "", arg + else: + raise ParseError(f"未知参数:{arg}") + + try: + result.setdefault(prompt, holder.type(x)) # 多个参数只取第1个 + except ParseError: + raise + except Exception: + raise ParseError(f"解析{holder.tip}失败") + + # 检查所有参数是否以赋值 + for prompt, holder in self.arg_dict.items(): + if prompt not in result: + if holder.default is None: # 缺失必要参数 抛异常 + msg = f"缺少参数:{holder.tip}" + raise ParseError(msg) + else: + result[prompt] = holder.default + + return result diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/battlemaster.py b/hoshino/modules/pcrclanbattle/clanbattlev3/battlemaster.py index 9d4fc4133..bc9c058e7 100644 --- a/hoshino/modules/pcrclanbattle/clanbattlev3/battlemaster.py +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/battlemaster.py @@ -1,109 +1,57 @@ import os import sqlite3 -from datetime import datetime, timedelta, timezone -from .model import * +from datetime import datetime +from . import helper +from .table import * -DB_FOLDER = os.path.expanduser('~/.hoshino/clanbattlev3/') +DB_FOLDER = os.path.expanduser("~/.hoshino/clanbattlev3/") os.makedirs(DB_FOLDER, exist_ok=True) -def get_yyyymmdd(time, zone_num:int=8): - '''返回time对应的会战年月日。 - - 其中,年月为该期会战的年月;日为刷新周期对应的日期。 - 会战为每月最后一星期,编程时认为mm月的会战一定在mm月20日至mm+1月10日之间,每日以5:00 UTC+8为界。 - 注意:返回的年月日并不一定是自然时间,如2019年9月2日04:00:00我们认为对应2019年8月会战,日期仍为1号,将返回(2019,8,1) - ''' - # 日台服均为当地时间凌晨5点更新,故减5 - time = time.astimezone(timezone(timedelta(hours=zone_num - 5))) - yyyy = time.year - mm = time.month - dd = time.day - if dd < 20: - mm = mm - 1 - if mm < 1: - mm = 12 - yyyy = yyyy - 1 - return (yyyy, mm, dd) - - class BattleMaster: - def __init__(self, gid, time): + def __init__(self, gid, time=None): self.gid = gid - self.yyyy, self.mm, _ = get_yyyymmdd(time) + self.year, self.month, _ = helper.yyyymmdd(time or datetime.now()) + self.clan = ClanTable() + self.member = MemberTable() + self.challenge = ChallengeTable(gid) + self.progress = ProgressTable(gid) + self.pause = PauseTable(gid) + self.sos = SosTable() + self.sl = SlTable() + self.subr = SubrTable() self._create_tables() + def connect_clan_db(self): + db = os.path.join(DB_FOLDER, "clan.db") + return sqlite3.connect( + db, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES + ) - def _connect_clan_db(self): - db = os.path.join(DB_FOLDER, 'clan.db') - return sqlite3.connect(db, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES) - - - def _connect_battle_db(self): - db = os.path.join(DB_FOLDER, f'battle{self.yyyy:04d}{self.mm:02d}.db') - return sqlite3.connect(db, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES) - + def connect_battle_db(self): + db = os.path.join(DB_FOLDER, f"battle{self.year:04d}{self.month:02d}.db") + return sqlite3.connect( + db, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES + ) def _create_tables(self): - with self._connect_clan_db() as conn: - sql = "CREATE TABLE IF NOT EXISTS clan" \ - "(gid INT PRIMARY KEY NOT NULL, name TEXT NOT NULL, server INT NOT NULL)" - conn.execute(sql) - sql = "CREATE TABLE IF NOT EXISTS member" \ - "(gid INT NOT NULL, uid INT NOT NULL, name TEXT NOT NULL, PRIMARY KEY (gid, uid))" - conn.execute(sql) - - with self._connect_battle_db() as conn: - sql = f"CREATE TABLE IF NOT EXISTS challenge_{self.gid}" \ - "(eid INTEGER PRIMARY KEY AUTOINCREMENT, uid INT NOT NULL, time DATETIME NOT NULL, round INT NOT NULL, boss INT NOT NULL, dmg INT NOT NULL, flag INT NOT NULL)" - conn.execute(sql) - sql = "CREATE TABLE IF NOT EXISTS progress" \ - "(gid INT PRIMARY KEY NOT NULL, round INT NOT NULL, boss INT NOT NULL, hp INT NOT NULL)" - conn.execute(sql) - sql = "CREATE TABLE IF NOT EXISTS pause" \ - "(gid INT NOT NULL, uid INT NOT NULL, dmg INT NOT NULL, second_left INT NOT NULL)" - conn.execute(sql) - sql = "CREATE TABLE IF NOT EXISTS sos" \ - "(gid INT NOT NULL, uid INT NOT NULL, time DATETIME NOT NULL, round INT NOT NULL, boss INT NOT NULL)" - conn.execute(sql) - sql = "CREATE TABLE IF NOT EXISTS sl" \ - "(gid INT NOT NULL, uid INT NOT NULL, time DATETIME NOT NULL, round INT NOT NULL, boss INT NOT NULL)" - conn.execute(sql) - sql = "CREATE TABLE IF NOT EXISTS subscribe" \ - "(gid INT NOT NULL, uid INT NOT NULL, time DATETIME NOT NULL, round INT NOT NULL, boss INT NOT NULL)" - conn.execute(sql) - - - def get_clan(self): - with self._connect_clan_db() as conn: - clan = conn.execute("SELECT gid, name, server FROM clan WHERE gid=?", - (self.gid, )).fetchone() - return Clan(*clan) if clan else None - - - def add_clan(self, clan: Clan): - with self._connect_clan_db() as conn: - conn.execute("INSERT OR REPLACE INTO clan (gid, name, server) VALUES (?, ?, ?)", - (clan.gid, clan.name, clan.server)) - - - def get_member(self, uid): - with self._connect_clan_db() as conn: - m = conn.execute("SELECT uid, gid, name FROM member WHERE gid=? and uid=?", - (self.gid, uid)).fetchone() - return Member(*m) if m else None - - - def add_member(self, member: Member): - with self._connect_clan_db() as conn: - conn.execute("INSERT OR REPLACE INTO member (uid, gid, name) VALUES (?, ?, ?)", - (member.uid, member.gid, member.name)) - - - def del_member(self, uid): - with self._connect_clan_db() as conn: - conn.execute("DELETE FROM member WHERE uid=?", (uid, )) - - - - \ No newline at end of file + with self.connect_clan_db() as conn: + self.clan.create(conn) + self.member.create(conn) + + with self.connect_battle_db() as conn: + self.challenge.create(conn) + self.progress.create(conn) + self.pause.create(conn) + self.sos.create(conn) + self.sl.create(conn) + self.subr.create(conn) + + def uid2name(self, conn, uids): + names = [] + for uid in uids: + x = conn.execute( + "SELECT name FROM member WHERE gid=? AND uid=?", (self.gid, uid) + ).fetchone() + names.append(x[0] if x else str(uid)) + return names diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3.py b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3.py deleted file mode 100644 index 9d91cfa8e..000000000 --- a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3.py +++ /dev/null @@ -1,240 +0,0 @@ -from datetime import datetime -from hoshino import Service, priv -from hoshino.typing import * - -from .battlemaster import BattleMaster as Master -from .const import * -from .model import * -from .dtype import * - -sv = Service('clanbattlev3', bundle='pcr会战', help_='Hoshino会战管理v3(还没写完酷Q就没了 悲)') - -ERROR_CLAN_NOTFOUND = f'公会未初始化:请群管理发送"建会日/台/B服+公会名"' -ERROR_ZERO_MEMBER = f'公会内无成员:请使用"入会"或"批量入会"以添加' -ERROR_MEMBER_NOTFOUND = f'未找到成员:请使用"入会+昵称"以添加' -ERROR_PERMISSION_DENIED = '权限不足:需*群管理*以上权限' - -# =============== helper functions ================= # - -async def _check_clan(bot, ev, bm: Master) -> Clan: - clan = bm.get_clan() - if not clan: - await bot.finish(ev, ERROR_CLAN_NOTFOUND) - return clan - -async def _check_member(bot, ev, bm: Master, uid:int, tip=None) -> Member: - mem = bm.get_member(uid) - if not mem: - await bot.finish(ev, tip or ERROR_MEMBER_NOTFOUND) - return mem - -async def _check_admin(bot, ev, tip:str=''): - if not priv.check_priv(ev, priv.ADMIN): - await bot.finish(ev, ERROR_PERMISSION_DENIED + tip) - -def _get_at(ev: CQEvent): - for seg in ev.message: - if seg.type == 'at': - at = int(seg.data['qq']) - if at != ev.self_id: - return at - return None - - -# ================================================== # - -async def add_clan(bot, ev: CQEvent, server): - await _check_admin(bot, ev, '才能建会') - gid = ev.group_id - name = one_line_str(ev.message.extract_plain_text()) - if not name: - ginfo = await bot.get_group_info(self_id=ev.self_id, group_id=ev.group_id) - name = ginfo['group_name'] - if not name: - name = 'Unknown' - clan = Clan(gid, name, server) - bm = Master(gid, datetime.now()) - bm.add_clan(clan) - msg = f"公会信息已登记!\n公会名:{name}\n服务器:{server_name(server)}" - await bot.send(ev, msg) - - -@sv.on_prefix(('建会日服', '建立日服公会')) -async def _(bot, ev: CQEvent): - await add_clan(bot, ev, SERVER.JP) - -@sv.on_prefix(('建会台服', '建立台服公会')) -async def _(bot, ev): - await add_clan(bot, ev, SERVER.TW) - -@sv.on_prefix(('建会B服', '建会b服', '建会陆服', '建立B服公会', '建立b服公会', '建立陆服公会')) -async def _(bot, ev): - await add_clan(bot, ev, SERVER.CN) - - -@sv.on_prefix(('入会', '加入公会')) -async def add_member(bot, ev: CQEvent): - gid = ev.group_id - bm = Master(gid, datetime.now()) - clan = await _check_clan(bot, ev, bm) - uid = _get_at(ev) or ev.user_id - if uid != ev.user_id: - await _check_admin(bot, ev) - minfo = await bot.get_group_member_info(self_id=ev.self_id, group_id=gid, user_id=uid) - else: - minfo = ev.sender - name = one_line_str(ev.message.extract_plain_text()) or \ - one_line_str(minfo['card']) or one_line_str(minfo['nickname']) or '祐樹' - member = Member(uid, gid, name) - bm.add_member(member) - msg = f"欢迎{MessageSegment.at(uid)}加入{clan.name}!\n骑士名:{name}" - await bot.send(ev, msg) - - -@sv.on_prefix('退会') -async def del_member(bot, ev: CQEvent): - gid = ev.group_id - bm = Master(gid, datetime.now()) - arg = ev.message.extract_plain_text().strip() - if arg: - try: - uid = int(arg) - except ValueError: - await bot.finish(ev, '参数错误!\n发送"退会"退出自己\n发送"退会+QQ号"退出其他成员') - else: - uid = _get_at(ev) or ev.user_id - mem = await _check_member(bot, ev, bm, uid, '公会内无此成员') - if uid != ev.user_id: - await _check_admin(bot, ev, '才能移出其他成员') - bm.del_member(uid) - msg = f"已将{mem.name}移出本会" - await bot.send(ev, msg) - - -@sv.on_fullmatch('查看成员') -async def list_member(bot, ev): - pass - -@sv.on_fullmatch(('一键入会', '批量入会', '加入全部成员')) -async def add_member_all(bot, ev): - pass - -@sv.on_fullmatch(('清空成员', '删除全部成员')) -async def clear_member(bot, ev): - pass - - -# ================================================== # - - -@sv.on_prefix(('报刀', '出刀')) -async def add_challenge(bot, ev): - pass - - -@sv.on_prefix(('尾刀', '收尾')) -async def _(bot, ev): - await bot.send(ev, '避免误触,击杀Boss请发送"确认尾刀"') - -@sv.on_prefix(('确认尾刀', '确认秒杀', '确认收尾')) -async def add_challenge_last(bot, ev): - pass - - -@sv.on_prefix(('掉刀', '滑刀')) -async def add_challenge_timeout(bot, ev): - pass - - - -@sv.on_prefix(('删刀', '撤销')) -async def del_challenge(bot, ev): - pass - - -@sv.on_fullmatch(('状态', '进度')) -async def show_progress(bot, ev): - pass - - # TODO - # 周目 Boss 分数倍率 血量 - # 出刀队列(含白嫖刀计算) - # 预约队列 - # 挂树队列 - - - -@sv.on_prefix('预约') -async def add_subr(bot, ev): - pass - - -@sv.on_prefix(('取消', '取消预约')) -async def del_subr(bot, ev): - pass - - -@sv.on_prefix(('查', '查预约')) -async def list_subr(bot, ev): - pass - - -@sv.on_prefix(('清预约', '清空预约', '清理预约')) -async def clear_subr(bot, ev): - pass - - -@sv.on_prefix('预约上限') -async def set_subr_limit(bot, ev): - pass - -@sv.on_fullmatch(('挂树', '上树')) -async def add_sos(bot, ev): - pass - - -@sv.on_fullmatch('查树') -async def list_sos(bot, ev): - pass - - -@sv.on_fullmatch(('我进了', '那我进了', '申请出刀', '锁定')) -async def join_battle_queue(bot, ev): - pass - -@sv.on_fullmatch(('我出了', '我不进了', '解锁')) -async def quit_battle_queue(bot, ev): - pass - - -async def log_pause(bot, ev, dmg:int=0, time_left:int=0): - pass - -@sv.on_prefix('暂停') -async def _(bot, ev): - await log_pause(bot, ev) - -@sv.on_rex(r'^(?P\d+)s\s*(?P\d+)w$') -@sv.on_rex(r'^(?P\d+)w\s*(?P\d+)s$') -async def _(bot, ev): - await log_pause(bot, ev) - - -@sv.on_fullmatch('sl') -async def log_sl(bot, ev): - pass - - -@sv.on_fullmatch('统计') -async def stat(bot, ev): - pass - -@sv.on_fullmatch('查刀') -async def list_remain(bot, ev): - pass - -@sv.on_fullmatch('催刀') -async def urge_remain(bot, ev): - pass - - diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/__init__.py b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/__init__.py new file mode 100644 index 000000000..e817eb123 --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/__init__.py @@ -0,0 +1,5 @@ +from hoshino import Service + +sv = Service("clanbattlev3", bundle="pcr会战", help_="Hoshino会战管理v3(还没写完酷Q就没了 悲)") + +from . import clan, challenge, battle_queue, sl, sos, subr, stat, progress diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/battle_queue.py b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/battle_queue.py new file mode 100644 index 000000000..527397b3f --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/battle_queue.py @@ -0,0 +1,59 @@ +from hoshino.typing import * + +from .. import renderer +from ..battlemaster import BattleMaster as Master +from ..dtype import * +from ..helper import * +from ..model import * +from . import sv + + +@sv.on_fullmatch(("我进了", "那我进了", "申请出刀", "锁定")) +@handle_clanbattle_error +async def join_battle_queue(bot, ev: CQEvent): + bm = Master(ev.group_id) + with bm.connect_clan_db() as conn: + check_clan(bm, conn) + check_member(bm, conn, ev.user_id) + with bm.connect_battle_db() as conn: + bm.pause.add(conn, PauseRecord(ev.user_id, 0, 0)) + count = bm.pause.count(conn) + msg = ["已登记进本", f"┗当前战斗人数:{count}"] + await bot.send(ev, "\n".join(msg)) + + +@sv.on_fullmatch(("我出了", "我不进了", "解锁")) +@handle_clanbattle_error +async def quit_battle_queue(bot, ev: CQEvent): + bm = Master(ev.group_id) + with bm.connect_clan_db() as conn: + check_clan(bm, conn) + check_member(bm, conn, ev.user_id) + with bm.connect_battle_db() as conn: + bm.pause.remove(conn, ev.user_id) + count = bm.pause.count(conn) + msg = ["已登记出本", f"┗当前战斗人数:{count}"] + await bot.send(ev, "\n".join(msg)) + + +@sv.on_rex(r"^(?P\d{0,3}?)(s|\s*)(?P\d+)w$") +@sv.on_rex(r"^(?P\d+)w\s*(?P\d{0,3})s?$") +@handle_clanbattle_error +async def log_pause(bot, ev: CQEvent): + bm = Master(ev.group_id) + dmg = int(ev.match.group("dmg")) * 10000 + time = ev.match.group("t") + time = int(time) if time else 0 + if time >= 100: + time -= 40 # convert 1min to 60s + pause = PauseRecord(ev.user_id, dmg, time) + with bm.connect_clan_db() as conn1, bm.connect_battle_db() as conn2: + clan = check_clan(bm, conn1) + check_member(bm, conn1, ev.user_id) + bm.pause.add(conn2, pause) + pauses = bm.pause.list(conn2) + prog = bm.progress.get(conn2) + names = bm.uid2name(conn1, [p.uid for p in pauses]) + + msg = renderer.pause_list(names, pauses, clan, prog, "已登记出本前暂停") + await bot.send(ev, msg) diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/challenge.py b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/challenge.py new file mode 100644 index 000000000..6940f2574 --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/challenge.py @@ -0,0 +1,133 @@ +from datetime import datetime + +from hoshino.typing import CQEvent + +from .. import renderer +from ..argparser import ArgParser +from ..battlemaster import BattleMaster as Master +from ..const import * +from ..dtype import * +from ..exception import * +from ..helper import * +from ..model import * +from . import sv + + +@handle_clanbattle_error +async def handle_challenge(bot, ev: CQEvent, ch: Challenge): + bm = Master(ev.group_id) + with bm.connect_clan_db() as conn: + clan = check_clan(bm, conn) + mem = check_member(bm, conn, ch.uid) + with bm.connect_battle_db() as conn: + prog = bm.progress.get(conn) + ch.round = prog.round + ch.boss = prog.boss + if ch.flag == CHALLENGE.LAST: + ch.dmg = prog.hp + prog.round, prog.boss = next_round_boss(prog.round, prog.boss) + prog.hp = get_boss_hp(prog.round, prog.boss, clan.server) + bm.pause.clear(conn) + else: + prog.hp -= ch.dmg + bm.pause.remove(conn, ch.uid) + if prog.hp <= 0: + await bot.finish(ev, '伤害超出当前血量 击杀请用"确认尾刀"') + if bm.challenge.get_latest_flag(conn, ch.uid) == CHALLENGE.LAST: + ch.flag = CHALLENGE.EXT + bm.challenge.add(conn, ch) + bm.progress.add(conn, prog) + msg = [ + f"记录编号{ch.eid}", + f"┣{mem.name} {flag_name(ch.flag)}", + f"┗造成 {ch.dmg:,d} 点伤害", + renderer.progress(prog, clan) + ] + await bot.send(ev, '\n'.join(msg)) + + +@sv.on_prefix(("报刀", "出刀")) +@handle_clanbattle_error +async def add_challenge(bot, ev: CQEvent): + dmg = damage_int(plain_text(ev.message)) + if dmg < 1000: + await bot.finish(ev, "干嘛呢?弟弟") + + ch = Challenge( + eid=0, + uid=get_at(ev) or ev.user_id, + time=datetime.now(), + round=0, + boss=0, + dmg=dmg, + flag=CHALLENGE.NORM, + ) + await handle_challenge(bot, ev, ch) + + +@sv.on_fullmatch(("尾刀", "收尾")) +async def _(bot, ev): + await bot.send(ev, '避免误触,击杀Boss请发送"确认尾刀"') + + +@sv.on_prefix(("确认尾刀", "确认秒杀", "确认收尾")) +async def add_challenge_last(bot, ev): + ch = Challenge( + eid=0, + uid=get_at(ev) or ev.user_id, + time=datetime.now(), + round=0, + boss=0, + dmg=0, + flag=CHALLENGE.LAST, + ) + await handle_challenge(bot, ev, ch) + + +@sv.on_prefix(("掉刀", "滑刀")) +async def add_challenge_timeout(bot, ev): + ch = Challenge( + eid=0, + uid=get_at(ev) or ev.user_id, + time=datetime.now(), + round=0, + boss=0, + dmg=0, + flag=CHALLENGE.TIMEOUT, + ) + await handle_challenge(bot, ev, ch) + + +@sv.on_prefix(("删刀", "撤销")) +@handle_clanbattle_error +async def del_challenge(bot, ev): + pass + # TODO + + +parser = ArgParser() +parser.add_arg("", "伤害值", damage_int) +parser.add_arg("R", "R周目数", round_code) +parser.add_arg("B", "B王编号", boss_code) +parser.add_arg("@", "@代报QQ", int) + + +def parse_challenge(ev: CQEvent) -> Challenge: + args = plain_text(ev.message).split() + r = parser.parse(args) + return Challenge( + eid=0, + uid=r["@"] or get_at(ev) or ev.user_id, + time=datetime.now(), + round=r["R"], + boss=r["B"], + dmg=r[""], + flag=0, + ) + + +@sv.on_prefix("补报") +@handle_clanbattle_error +async def add_old_challenge(bot, ev): + pass + # TODO diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/clan.py b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/clan.py new file mode 100644 index 000000000..fb6938426 --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/clan.py @@ -0,0 +1,112 @@ +from datetime import datetime + +from hoshino.typing import CQEvent, MessageSegment + +from ..battlemaster import BattleMaster as Master +from ..const import * +from ..dtype import * +from ..exception import * +from ..helper import * +from ..model import * +from . import sv + + +@handle_clanbattle_error +async def add_clan(bot, ev: CQEvent, server): + check_admin(ev, "才能建会") + gid = ev.group_id + name = one_line_str(plain_text(ev.message)) + if not name: + ginfo = await bot.get_group_info(self_id=ev.self_id, group_id=ev.group_id) + name = ginfo["group_name"] + if not name: + name = "Unknown" + + clan = Clan(gid, name, server) + bm = Master(gid) + with bm.connect_clan_db() as conn: + bm.clan.add(conn, clan) + msg = f"公会信息已登记!\n公会名:{name}\n服务器:{server_name(server)}" + await bot.send(ev, msg) + + +@sv.on_prefix(("建会日服", "建立日服公会")) +async def _(bot, ev: CQEvent): + await add_clan(bot, ev, SERVER.JP) + + +@sv.on_prefix(("建会台服", "建立台服公会")) +async def _(bot, ev): + await add_clan(bot, ev, SERVER.TW) + + +@sv.on_prefix(("建会B服", "建会b服", "建会陆服", "建立B服公会", "建立b服公会", "建立陆服公会")) +async def _(bot, ev): + await add_clan(bot, ev, SERVER.BL) + + +@sv.on_prefix(("入会", "加入公会")) +@handle_clanbattle_error +async def add_member(bot, ev: CQEvent): + gid = ev.group_id + bm = Master(gid) + uid = get_at(ev) or ev.user_id + if uid != ev.user_id: + check_admin(ev, "才能邀请其他人") + minfo = await bot.get_group_member_info( + self_id=ev.self_id, group_id=gid, user_id=uid + ) + else: + minfo = ev.sender + name = ( + one_line_str(plain_text(ev.message)) + or one_line_str(minfo["card"]) + or one_line_str(minfo["nickname"]) + or "祐樹" + ) + with bm.connect_clan_db() as conn: + clan = check_clan(bm, conn) + member = Member(uid, gid, name) + bm.member.add(conn, member) + msg = f"欢迎加入{clan.name}!\n骑士名:{name}" + await bot.send(ev, msg) + + +@sv.on_prefix("退会") +@handle_clanbattle_error +async def del_member(bot, ev: CQEvent): + gid = ev.group_id + bm = Master(gid) + arg = plain_text(ev.message).strip() + if arg: + try: + uid = int(arg) + except ValueError: + raise ParseError('命令解析失败!发送"退会+QQ号"移出其他成员') + else: + uid = get_at(ev) or ev.user_id + + with bm.connect_clan_db() as conn: + mem = check_member(bm, conn, uid, "公会内无此成员") + if uid != ev.user_id: + check_admin(ev, "才能移出其他成员") + bm.member.remove(conn, (bm.gid, uid)) + msg = f"已将{mem.name}移出本会" + await bot.send(ev, msg) + + +@sv.on_fullmatch("查看成员") +async def list_member(bot, ev): + pass + # TODO + + +@sv.on_fullmatch(("一键入会", "批量入会", "加入全部成员")) +async def add_member_all(bot, ev): + pass + # TODO + +@sv.on_fullmatch(("清空成员", "删除全部成员")) +async def clear_member(bot, ev): + pass + # TODO diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/progress.py b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/progress.py new file mode 100644 index 000000000..3dbf720d2 --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/progress.py @@ -0,0 +1,30 @@ +from hoshino.typing import * + +from .. import renderer +from ..battlemaster import BattleMaster as Master +from ..const import * +from ..dtype import * +from ..helper import * +from ..model import * +from . import sv + + +@sv.on_fullmatch(("状态", "进度", "查当前")) +@handle_clanbattle_error +async def show_progress(bot, ev: CQEvent): + bm = Master(ev.group_id) + with bm.connect_clan_db() as conn1, bm.connect_battle_db() as conn2: + clan = check_clan(bm, conn1) + prog = bm.progress.get(conn2) + pauses = bm.pause.list(conn2) + names = bm.uid2name(conn1, [p.uid for p in pauses]) + + msg = [ + renderer.progress(prog, clan), + renderer.pause_list(names, pauses, clan, prog, f'战斗中:{len(pauses)}人'), + # TODO: 预约队列 + # TODO: 挂树队列 + ] + await bot.send(ev, '\n'.join(msg)) + +# TODO: 调整进度 diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/sl.py b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/sl.py new file mode 100644 index 000000000..8ae1f2dd3 --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/sl.py @@ -0,0 +1,16 @@ +from datetime import datetime +from hoshino import Service, priv +from hoshino.typing import * + +from ..battlemaster import BattleMaster as Master +from ..const import * +from ..model import * +from ..dtype import * + +from . import sv + + +@sv.on_fullmatch('sl') +async def log_sl(bot, ev): + pass + diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/sos.py b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/sos.py new file mode 100644 index 000000000..78520f569 --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/sos.py @@ -0,0 +1,21 @@ +from datetime import datetime +from hoshino import Service, priv +from hoshino.typing import * + +from ..battlemaster import BattleMaster as Master +from ..const import * +from ..model import * +from ..dtype import * +from . import sv + + +@sv.on_fullmatch(('挂树', '上树')) +async def add_sos(bot, ev): + pass + + +@sv.on_fullmatch('查树') +async def list_sos(bot, ev): + pass + + diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/stat.py b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/stat.py new file mode 100644 index 000000000..392563f2c --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/stat.py @@ -0,0 +1,28 @@ +from hoshino.typing import * + +from ..battlemaster import BattleMaster as Master +from ..const import * +from ..model import * +from ..dtype import * +from ..helper import * + +from . import sv + +@sv.on_fullmatch("统计") +async def stat(bot, ev): + pass + + +@sv.on_fullmatch("查刀") +async def list_remain(bot, ev): + pass + + +@sv.on_fullmatch("催刀") +async def urge_remain(bot, ev): + pass + + +@sv.on_prefix("离职报告") +async def retire_report(bot, ev): + pass diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/subr.py b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/subr.py new file mode 100644 index 000000000..947be7c6a --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/cmdv3/subr.py @@ -0,0 +1,33 @@ +from datetime import datetime +from hoshino import Service, priv +from hoshino.typing import * + +from ..battlemaster import BattleMaster as Master +from ..const import * +from ..model import * +from ..dtype import * +from . import sv + +@sv.on_prefix('预约') +async def add_subr(bot, ev): + pass + + +@sv.on_prefix(('取消', '取消预约')) +async def del_subr(bot, ev): + pass + + +@sv.on_prefix(('查', '查预约')) +async def list_subr(bot, ev): + pass + + +@sv.on_prefix(('清预约', '清空预约', '清理预约')) +async def clear_subr(bot, ev): + pass + + +@sv.on_prefix('预约上限') +async def set_subr_limit(bot, ev): + pass diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/const.py b/hoshino/modules/pcrclanbattle/clanbattlev3/const.py index e1027d31f..243a3a7ac 100644 --- a/hoshino/modules/pcrclanbattle/clanbattlev3/const.py +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/const.py @@ -1,10 +1,11 @@ class SERVER: JP = 0x00 TW = 0x01 - CN = 0x02 + BL = 0x02 + class CHALLENGE: - NORM = 0x00 - LAST = 0x01 - EXT = 0x02 - TIMEOUT = 0x04 \ No newline at end of file + NORM = 0x00 + LAST = 0x01 + EXT = 0x02 + TIMEOUT = 0x04 diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/dtype.py b/hoshino/modules/pcrclanbattle/clanbattlev3/dtype.py index 3f91281f2..0bbd3d44b 100644 --- a/hoshino/modules/pcrclanbattle/clanbattlev3/dtype.py +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/dtype.py @@ -2,64 +2,74 @@ from hoshino import util from .exception import ParseError -from .const import SERVER +from .const import * -_unit_rate = {'': 1, 'k': 1000, 'w': 10000, '千': 1000, '万': 10000} -_rex_dint = re.compile(r'^(\d+)([wk千万]?)$', re.I) -_rex1_bcode = re.compile(r'^老?([1-5])王?$') -_rex2_bcode = re.compile(r'^老?([一二三四五])王?$') -_rex_rcode = re.compile(r'^[1-9]\d{0,2}$') +_unit_rate = {"": 1, "k": 1000, "w": 10000, "千": 1000, "万": 10000} +_rex_dint = re.compile(r"^(\d+)([wk千万]?)$", re.I) +_rex1_bcode = re.compile(r"^老?([1-5])王?$") +_rex2_bcode = re.compile(r"^老?([一二三四五])王?$") +_rex_rcode = re.compile(r"^[1-9]\d{0,2}$") -def damage_int(x:str) -> int: - x = util.normalize_str(x) + +def damage_int(x: str) -> int: + x = util.normalize_str(x.strip()) if m := _rex_dint.match(x): x = int(m.group(1)) * _unit_rate[m.group(2).lower()] if x < 100000000: return x - raise ParseError('伤害值不合法 伤害值应为小于一亿的非负整数') + raise ParseError("伤害值不合法 应为小于一亿的非负整数") -def boss_code(x:str) -> int: - x = util.normalize_str(x) +def boss_code(x: str) -> int: + x = util.normalize_str(x.strip()) if m := _rex1_bcode.match(x): return int(m.group(1)) elif m := _rex2_bcode.match(x): - return '零一二三四五'.find(m.group(1)) - raise ParseError('Boss编号不合法 应为1-5的整数') + return "零一二三四五".find(m.group(1)) + raise ParseError("Boss编号不合法 应为1-5的整数") -def round_code(x:str) -> int: - x = util.normalize_str(x) +def round_code(x: str) -> int: + x = util.normalize_str(x.strip()) if _rex_rcode.match(x): return int(x) - raise ParseError('周目数不合法 应为不大于999的非负整数') + raise ParseError("周目数不合法 应为不大于999的非负整数") -def server_code(x:str) -> int: - x = util.normalize_str(x) - if x in ('jp', '日', '日服'): +def server_code(x: str) -> int: + x = util.normalize_str(x.strip()) + if x in ("jp", "日", "日服"): return SERVER.JP - elif x in ('tw', '台', '台服'): + elif x in ("tw", "台", "台服"): return SERVER.TW - elif x in ('cn', '国', '国服', 'b', 'b服'): - return SERVER.CN - raise ParseError('未知服务器地区 请用jp/tw/cn') + elif x in ("cn", "国", "国服", "b", "b服"): + return SERVER.BL + raise ParseError("未知服务器地区 请用jp/tw/b") -def server_name(x:int) -> str: +def server_name(x: int) -> str: if x == SERVER.JP: - return '日服' - elif x == SERVER.TW: - return '台服' - elif x == SERVER.CN: - return 'B服' - else: - return 'unknown' + return "日服" + if x == SERVER.TW: + return "台服" + if x == SERVER.BL: + return "B服" + return "unknown" + -def one_line_str(s:str) -> str: - return s.replace('\n', ' ').strip() +def boss_name(x: int) -> str: + if isinstance(x, int) and 1 <= x <= 5: + return '零一二三四五'[x] + '王' + return '?王' -__all__ = [ - 'damage_int', 'boss_code', 'round_code', 'server_code', 'server_name', 'one_line_str' -] +def flag_name(x: int) -> str: + if x == CHALLENGE.NORM: + return "完整刀" + if x == CHALLENGE.LAST: + return "尾刀" + if x == CHALLENGE.EXT: + return "补时刀" + if x == CHALLENGE.TIMEOUT: + return "滑刀" + return "unknown" diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/exception.py b/hoshino/modules/pcrclanbattle/clanbattlev3/exception.py index ecefbfede..3606c87e1 100644 --- a/hoshino/modules/pcrclanbattle/clanbattlev3/exception.py +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/exception.py @@ -1,16 +1,19 @@ +import sqlite3 + + class ClanBattleError(Exception): - def __init__(self, msg, *msgs): - self._msgs = [msg, *msgs] + def __init__(self, *msgs): + self.msg = msgs if msgs else [] def __str__(self): - return '\n'.join(self._msgs) + return "\n".join(self.msg) @property def message(self): return str(self) - def append(self, msg:str): - self._msgs.append(msg) + def append(self, msg: str): + self.msg.append(msg) class ParseError(ClanBattleError): @@ -29,5 +32,5 @@ class PermissionDeniedError(ClanBattleError): pass -class DatabaseError(ClanBattleError): +class DatabaseError(ClanBattleError, sqlite3.DatabaseError): pass diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/helper.py b/hoshino/modules/pcrclanbattle/clanbattlev3/helper.py new file mode 100644 index 000000000..7de6fc239 --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/helper.py @@ -0,0 +1,125 @@ +from datetime import timedelta, timezone +from functools import wraps + +from hoshino import priv +from hoshino.config import clanbattle as cfg +from hoshino.typing import CQEvent + +from .const import * +from .exception import * +from .model import * + +ERROR_CLAN_NOTFOUND = '公会未初始化:请群管理发送"建会日/台/B服+公会名"' +ERROR_ZERO_MEMBER = '公会内无成员:请使用"入会"或"批量入会"以添加' +ERROR_MEMBER_NOTFOUND = '未找到成员:请使用"入会+昵称"以添加' +ERROR_PERMISSION_DENIED = "权限不足:需*群管理*以上" + + +def check_clan(bm, conn) -> Clan: + clan = bm.clan.get(conn, bm.gid) + if not clan: + raise NotFoundError(ERROR_CLAN_NOTFOUND) + return clan + + +def check_member(bm, conn, uid: int, tip=None) -> Member: + mem = bm.member.get(conn, (bm.gid, uid)) + if not mem: + raise NotFoundError(tip or ERROR_MEMBER_NOTFOUND) + return mem + + +def check_admin(ev, tip: str = ""): + if not priv.check_priv(ev, priv.ADMIN): + raise PermissionDeniedError(ERROR_PERMISSION_DENIED + tip) + + +def one_line_str(s: str) -> str: + return s.replace("\n", " ").strip() + + +def plain_text(message) -> str: + return message.extract_plain_text() + + +def get_at(ev: CQEvent): + for seg in ev.message: + if seg.type == "at": + at = int(seg.data["qq"]) + if at != ev.self_id: + return at + return None + + +def get_at_list(ev: CQEvent): + ret = [] + for seg in ev.message: + if seg.type == "at": + at = int(seg.data["qq"]) + if at != ev.self_id: + ret.append(at) + return ret + + +def handle_clanbattle_error(func): + @wraps(func) + async def wrapper(bot, ev, *arg, **kwarg): + try: + return await func(bot, ev, *arg, **kwarg) + except ClanBattleError as e: + await bot.finish(ev, str(e) or e.__class__.__name__) + + return wrapper + + +def next_round_boss(round_, boss): + return (round_, boss + 1) if boss < 5 else (round_ + 1, 1) + + +def get_stage(round_): + return 4 if round_ >= 35 else 3 if round_ >= 11 else 2 if round_ >= 4 else 1 + + +def get_boss_hp(round_, boss, server): + stage = get_stage(round_) + if server == SERVER.JP: + hp = cfg.JP.BOSS_HP + elif server == SERVER.TW: + hp = cfg.TW.BOSS_HP + elif server == SERVER.BL: + hp = cfg.BL.BOSS_HP + else: + raise ValueError("Unknown server.") + return hp[stage - 1][boss - 1] + + +def yyyymmdd(time, zone_num: int = 8): + """返回time对应的会战年月日。 + + 其中,年月为该期会战的年月;日为刷新周期对应的日期。 + 会战为每月最后一星期,编程时认为mm月的会战一定在mm月20日至mm+1月10日之间,每日以5:00 UTC+8为界。 + 注意:返回的年月日并不一定是自然时间,如2019年9月2日04:00:00我们认为对应2019年8月会战,日期仍为1号,将返回(2019,8,1) + """ + # 日台服均为当地时间凌晨5点更新,故减5 + time = time.astimezone(timezone(timedelta(hours=zone_num - 5))) + yyyy = time.year + mm = time.month + dd = time.day + if dd < 20: + mm = mm - 1 + if mm < 1: + mm = 12 + yyyy = yyyy - 1 + return (yyyy, mm, dd) + + +def get_t_alpha(clan): + return 10 if clan.server == SERVER.BL else 20 + + +def condition_full_refund(dmg, hp, t_alpha): + return dmg > 90 * hp / (91 + t_alpha) + + +def dmg_need_for_full_refund(dmg, hp, t_alpha): + return int(1 + hp - dmg * (t_alpha + 1) / 90) diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/model.py b/hoshino/modules/pcrclanbattle/clanbattlev3/model.py index b1540c4de..829b736b4 100644 --- a/hoshino/modules/pcrclanbattle/clanbattlev3/model.py +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/model.py @@ -1,62 +1,65 @@ from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import datetime + @dataclass class Clan: - gid:int - name:str - server:int + gid: int + name: str + server: int @dataclass class Member: - uid:int - gid:int - name:str + uid: int + gid: int + name: str + @dataclass class Challenge: - eid:int - uid:int - time:datetime - round:int - boss:int - dmg:int - flag:int + eid: int + uid: int + time: datetime + round: int + boss: int + dmg: int + flag: int @dataclass class Progress: - round:int - boss:int - hp:int + round: int + boss: int + hp: int + @dataclass class PauseRecord: - uid:int - dmg:int - second_left:int + uid: int + dmg: int + second_left: int @dataclass class Subscribe: - uid:int - round:int - boss:int - memo:str + uid: int + round: int + boss: int + memo: str @dataclass class SosRecord: - uid:int - time:datetime - round:int - boss:int + uid: int + time: datetime + round: int + boss: int @dataclass class SLRecord: - uid:int - time:datetime - round:int - boss:int + uid: int + time: datetime + round: int + boss: int diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/renderer.py b/hoshino/modules/pcrclanbattle/clanbattlev3/renderer.py new file mode 100644 index 000000000..384b53089 --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/renderer.py @@ -0,0 +1,49 @@ +from hoshino import get_bot, priv +from hoshino.config import clanbattle as cfg +from hoshino.typing import CQEvent, List + +from .const import * +from .exception import * +from .model import * +from .dtype import * +from .helper import * + + +def render_list(lines): + n = len(lines) + if n == 0: + return "" + if n == 1: + return "┗" + lines[0] + return "┣" + "\n┣".join(lines[:-1]) + "\n┗" + lines[-1] + + +def pause_list(names, pauses: List[PauseRecord], clan, prog, prompt): + n = len(pauses) + if not n: + return prompt + lines = [] + for i in range(n): + p = pauses[i] + line = names[i] + if p.dmg: + line += f":{(p.dmg // 10000):d}w" + if p.second_left: + line += f" {p.second_left}s" + lines.append(line) + msg = [prompt, render_list(lines)] + + t_alpha = get_t_alpha(clan) + dmg = pauses[0].dmg + if condition_full_refund(dmg, prog.hp, t_alpha): + dmg_need = dmg_need_for_full_refund(dmg, prog.hp, t_alpha) + msg.append(f"最高刀获满补时需垫{dmg_need:,d}点伤害" if dmg_need > 0 else "最高刀已可获满补时") + + return "\n".join(msg) + + +def progress(prog, clan): + return ( + f"当前{prog.round}周目 {boss_name(prog.boss)}\n" + f"┗{prog.hp:,d}/{get_boss_hp(prog.round, prog.boss, clan.server):,d}" + ) diff --git a/hoshino/modules/pcrclanbattle/clanbattlev3/table.py b/hoshino/modules/pcrclanbattle/clanbattlev3/table.py new file mode 100644 index 000000000..f14ae868a --- /dev/null +++ b/hoshino/modules/pcrclanbattle/clanbattlev3/table.py @@ -0,0 +1,188 @@ +from .model import * + + +class BaseTable: + def create(self, conn): + raise NotImplementedError + + def add(self, conn, obj): + raise NotImplementedError + + def get(self, conn, idx): + raise NotImplementedError + + def remove(self, conn, idx): + raise NotImplementedError + + +class ClanTable(BaseTable): + def create(self, conn): + conn.execute( + "CREATE TABLE IF NOT EXISTS clan " + "(gid INT PRIMARY KEY NOT NULL, name TEXT NOT NULL, server INT NOT NULL)" + ) + + def add(self, conn, clan: Clan): + conn.execute( + "INSERT OR REPLACE INTO clan (gid, name, server) VALUES (?, ?, ?)", + (clan.gid, clan.name, clan.server), + ) + + def get(self, conn, gid): + x = conn.execute( + "SELECT gid, name, server FROM clan WHERE gid=?", (gid,) + ).fetchone() + return Clan(*x) if x else None + + def remove(self, conn, gid): + conn.execute("DELETE FROM clan WHERE gid=?", (gid,)) + + +class MemberTable(BaseTable): + def create(self, conn): + conn.execute( + "CREATE TABLE IF NOT EXISTS member " + "(gid INT NOT NULL, uid INT NOT NULL, name TEXT NOT NULL, " + "PRIMARY KEY (gid, uid))" + ) + + def add(self, conn, member: Member): + conn.execute( + "INSERT OR REPLACE INTO member (gid, uid, name) VALUES (?, ?, ?)", + (member.gid, member.uid, member.name), + ) + + def get(self, conn, guid): + x = conn.execute( + "SELECT uid, gid, name FROM member WHERE gid=? AND uid=?", guid + ).fetchone() + return Member(*x) if x else None + + def remove(self, conn, guid): + conn.execute("DELETE FROM member WHERE gid=? AND uid=?", guid) + + +class ChallengeTable(BaseTable): + def __init__(self, gid): + self.gid = gid + + @property + def table_name(self): + return f"challenge_{self.gid}" + + def create(self, conn): + conn.execute( + f"CREATE TABLE IF NOT EXISTS {self.table_name} " + "(eid INTEGER PRIMARY KEY AUTOINCREMENT, uid INT NOT NULL, " + "time DATETIME NOT NULL, round INT NOT NULL, boss INT NOT NULL, " + "dmg INT NOT NULL, flag INT NOT NULL)" + ) + + def add(self, conn, ch: Challenge): + cur = conn.execute( + f"INSERT OR REPLACE INTO {self.table_name} " + "(uid, time, round, boss, dmg, flag) VALUES (?, ?, ?, ?, ?, ?)", + (ch.uid, ch.time, ch.round, ch.boss, ch.dmg, ch.flag), + ) + ch.eid = cur.lastrowid + + def remove(self, conn, eid): + conn.execute(f"DELETE FROM {self.table_name} WHERE eid=?", (eid,)) + + def get_latest_flag(self, conn, uid): + x = conn.execute( + f"SELECT flag FROM {self.table_name} WHERE uid=? ORDER BY round DESC, boss DESC, time DESC", + (uid,), + ).fetchone() + return x[0] if x else 0 + + +class ProgressTable(BaseTable): + def __init__(self, gid): + self.gid = gid + + def create(self, conn): + conn.execute( + "CREATE TABLE IF NOT EXISTS progress " + "(gid INT PRIMARY KEY NOT NULL, round INT NOT NULL, " + "boss INT NOT NULL, hp INT NOT NULL)" + ) + + def add(self, conn, progress: Progress): + conn.execute( + "INSERT OR REPLACE INTO progress " + "(gid, round, boss, hp) VALUES (?, ?, ?, ?)", + (self.gid, progress.round, progress.boss, progress.hp), + ) + + def get(self, conn, gid=None): + x = conn.execute( + "SELECT round, boss, hp FROM progress WHERE gid=?", (gid or self.gid,) + ).fetchone() + return ( + Progress(*x) if x else Progress(1, 1, 6000000) + ) # FIXME: hp of r1b1 may change? + + +class PauseTable(BaseTable): + def __init__(self, gid): + self.gid = gid + + def create(self, conn): + conn.execute( + "CREATE TABLE IF NOT EXISTS pause" + "(gid INT NOT NULL, uid INT NOT NULL, dmg INT NOT NULL, second_left INT NOT NULL, " + "PRIMARY KEY (gid, uid))" + ) + + def add(self, conn, pause: PauseRecord): + conn.execute( + "INSERT OR REPLACE INTO pause (gid, uid, dmg, second_left) VALUES (?, ?, ?, ?)", + (self.gid, pause.uid, pause.dmg, pause.second_left), + ) + + def count(self, conn): + x = conn.execute( + "SELECT COUNT(*) FROM pause WHERE gid=?", (self.gid,) + ).fetchone() + return x[0] + + def remove(self, conn, uid): + conn.execute("DELETE FROM pause WHERE gid=? AND uid=?", (self.gid, uid)) + + def clear(self, conn): + conn.execute("DELETE FROM pause WHERE gid=?", (self.gid,)) + + def list(self, conn): + xx = conn.execute( + "SELECT uid, dmg, second_left FROM pause WHERE gid=? ORDER BY dmg DESC", + (self.gid,), + ).fetchall() + return [PauseRecord(*x) for x in xx] + + +class SosTable(BaseTable): + def create(self, conn): + conn.execute( + "CREATE TABLE IF NOT EXISTS sos" + "(gid INT NOT NULL, uid INT NOT NULL, time DATETIME NOT NULL, " + "round INT NOT NULL, boss INT NOT NULL)" + ) + + +class SlTable(BaseTable): + def create(self, conn): + conn.execute( + "CREATE TABLE IF NOT EXISTS sl" + "(gid INT NOT NULL, uid INT NOT NULL, time DATETIME NOT NULL, " + "round INT NOT NULL, boss INT NOT NULL)" + ) + + +class SubrTable(BaseTable): + def create(self, conn): + conn.execute( + "CREATE TABLE IF NOT EXISTS subscribe" + "(gid INT NOT NULL, uid INT NOT NULL, time DATETIME NOT NULL, " + "round INT NOT NULL, boss INT NOT NULL)" + )