Commit e1c5d619 authored by nanahira's avatar nanahira

first

parent 3c59938f
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
/data
/output
/config.yaml
.git*
Dockerfile
.dockerignore
webpack.config.js
dist/*
build/*
\ No newline at end of file
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
/data
/output
/config.yaml
\ No newline at end of file
stages:
- build
- deploy
variables:
GIT_DEPTH: "1"
build:
stage: build
tags:
- linux
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
upload_to_minio:
stage: deploy
dependencies:
- build
tags:
- linux
script:
- aws s3 --endpoint=https://minio.mycard.moe:9000 sync --delete dist/full/ s3://nanahira/koishi-plugin/myplugin
only:
- master
deploy_npm:
stage: deploy
dependencies:
- build
tags:
- linux
script:
- apt update;apt -y install coreutils
- echo $NPMRC | base64 --decode > ~/.npmrc
- npm publish . || true
only:
- master
/install-npm.sh
.git*
/data
/output
/config.yaml
.idea
.dockerignore
Dockerfile
\ No newline at end of file
{
"singleQuote": true,
"trailingComma": "all"
}
\ No newline at end of file
The MIT License (MIT)
Copyright (c) 2021 Nanahira
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.
#!/bin/bash
npm install --save \
source-map-support \
koishi-thirdeye
npm install --save-dev \
@types/node \
typescript \
'@typescript-eslint/eslint-plugin@^4.28.2' \
'@typescript-eslint/parser@^4.28.2 '\
'eslint@^7.30.0' \
'eslint-config-prettier@^8.3.0' \
'eslint-plugin-prettier@^3.4.0' \
prettier \
raw-loader \
ts-loader \
webpack \
webpack-cli \
koishi@next
This diff is collapsed.
{
"name": "koishi-plugin-tabulate",
"description": "YGOCore 战队联盟排表姬的 Koishi 插件",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"lint": "eslint --fix .",
"build": "webpack && env PACK_AWS=1 webpack"
},
"repository": {
"type": "git",
"url": "https://code.mycard.moe/3rdeye/koishi-plugin-tabulate.git"
},
"author": "Nanahira <nanahira@momobako.com>",
"license": "MIT",
"keywords": [
"Koishi.js",
"qqbot",
"cqhttp",
"onebot",
"ygo",
"yugioh",
"ygopro"
],
"bugs": {
"url": "https://code.mycard.moe/3rdeye/koishi-plugin-tabulate/issues"
},
"homepage": "https://code.mycard.moe/3rdeye/koishi-plugin-tabulate",
"dependencies": {
"class-transformer": "^0.4.0",
"koishi-thirdeye": "^1.0.3",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"source-map-support": "^0.5.20"
},
"peerDependencies": {
"koishi": "^4.0.0-beta.0"
},
"devDependencies": {
"@types/lodash": "^4.14.176",
"@types/node": "^16.11.7",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1",
"koishi": "^4.0.0-beta.0",
"prettier": "^2.4.1",
"raw-loader": "^4.0.2",
"ts-loader": "^9.2.6",
"typescript": "^4.4.4",
"webpack": "^5.64.0",
"webpack-cli": "^4.9.1",
"ws": "^8.2.3"
}
}
import 'source-map-support/register';
import { DefineSchema } from 'koishi-thirdeye';
export class TabulatePluginConfig {
@DefineSchema({ desc: '比赛的超时时间', default: 7200000 })
saveTime: number;
}
export type TabulatePluginConfigLike = Partial<TabulatePluginConfig>;
import { Random, User } from 'koishi';
import { Type } from 'class-transformer';
import 'reflect-metadata';
import moment from 'moment';
import _ from 'lodash';
export enum ReportScoreResult {
NotFound,
Continue,
NextRound,
Finish,
}
function generateSpaces(n: number) {
return _.range(n)
.map(() => ' ')
.join('');
}
export class Match {
scores: number[];
winner: string;
constructor(public players: string[]) {
if (!players) {
return;
}
this.scores = [0, 0];
}
foo() {
return 'bar';
}
setScore(scores: number[]) {
this.scores = scores;
const [scorea, scoreb] = scores;
if (scorea === scoreb) {
return;
} else if (scorea > scoreb) {
this.winner = this.players[0];
} else {
this.winner = this.players[1];
}
}
reportScore(name: string, self: number, oppo: number) {
const pos = this.players.findIndex(
(p) => name.startsWith(p) || name.endsWith(p),
);
if (pos === -1) {
return false;
}
const score: number[] = [];
score[pos] = self;
score[1 - pos] = oppo;
this.setScore(score);
return true;
}
isFinished() {
return this.winner || this.scores.some((s) => s !== 0);
}
format(leftMaxLength: number, rightMaxLength: number) {
const leftSpace = leftMaxLength - this.players[0].length + 2;
const rightSpace = rightMaxLength - this.players[1].length + 2;
return `${this.players[0]}${generateSpaces(leftSpace)}${this.scores[0]}:${
this.scores[1]
}${generateSpaces(rightSpace)}${this.players[1]}`;
}
}
export class Round {
@Type(() => Match)
matches: Match[];
constructor(_playerLists: string[][]) {
if (!_playerLists) {
return;
}
const playerLists = _playerLists.map((plist) => Random.shuffle(plist));
const matchCount = Math.min(...playerLists.map((l) => l.length));
this.matches = [];
for (let i = 0; i < matchCount; ++i) {
const [lista, listb] = playerLists;
this.matches.push(new Match([lista[i], listb[i]]));
}
}
reportScore(name: string, selfScore: number, oppoScore: number) {
const match = this.matches.find((m) =>
m.players.some((p) => name.startsWith(p) || name.endsWith(p)),
);
if (!match) {
return;
}
return match.reportScore(name, selfScore, oppoScore);
}
isAllFinished() {
return this.matches.every((m) => m.isFinished());
}
format(leftMaxLength: number, rightMaxLength: number) {
return this.matches
.map((m) => m.format(leftMaxLength, rightMaxLength))
.join('\n');
}
}
export class Game {
@Type(() => Round)
rounds: Round[];
constructor(
public location: string,
public rule: string,
public teama: string,
public teamb: string,
public playera: string[],
public playerb: string[],
) {
if (!teama) {
return;
}
this.rounds = [];
this.newRound();
}
newRound() {
if (this.rule === '人头赛' && this.rounds.length) {
return false;
}
const playerLists = [this.playera, this.playerb].map((plist) =>
plist.filter((p) => this.isPlayerAvailable(p)),
);
if (playerLists.some((l) => !l.length)) {
return false;
}
this.rounds.push(new Round(playerLists));
return true;
}
isPlayerAvailable(name: string) {
return !this.rounds.some((round) =>
round.matches.some(
(match) => match.players.includes(name) && match.winner !== name,
),
);
}
reportScore(name: string, selfScore: number, oppoScore: number) {
const round = this.rounds[this.rounds.length - 1];
const result = round.reportScore(name, selfScore, oppoScore);
if (!result) {
return ReportScoreResult.NotFound;
}
if (round.isAllFinished()) {
const nextRoundResult = this.newRound();
if (nextRoundResult) {
return ReportScoreResult.NextRound;
} else {
return ReportScoreResult.Finish;
}
} else {
return ReportScoreResult.Continue;
}
}
format() {
const allMatches = _.flatten(this.rounds.map((r) => r.matches));
const leftMaxLength = Math.max(
..._.map(allMatches, (m) => m.players[0].length),
);
const rightMaxLength = Math.max(
..._.map(allMatches, (m) => m.players[1].length),
);
const lines: string[] = [];
lines.push(`战队: ${this.teama} VS ${this.teamb}`);
lines.push(`时间: ${moment().format('YYYY-MM-DD')}`);
lines.push(`规则: ${this.rule}`);
lines.push(`地点: ${this.location}`);
lines.push('------------第一轮------------');
lines.push(this.rounds[0].format(leftMaxLength, rightMaxLength));
lines.push('------------第二轮------------');
lines.push(
this.rounds
.slice(1)
.map((r) => r.format(leftMaxLength, rightMaxLength))
.join('\n'),
);
return lines.join('\n');
}
getKey() {
return `${this.location}_${this.teama}_${this.teamb}`;
}
includesPendingPlayer(possibleNames: string[]) {
return possibleNames.find((name) =>
this.rounds[this.rounds.length - 1].matches.some((match) =>
match.players.some(
(playerName) =>
name.startsWith(playerName) || name.endsWith(playerName),
),
),
);
}
}
import 'source-map-support/register';
import { Context, Cache, Session } from 'koishi';
import { TabulatePluginConfig, TabulatePluginConfigLike } from './config';
import {
KoishiPlugin,
InjectConfig,
Inject,
OnApply,
UseEvent,
UseCommand,
CommandUsage,
CommandExample,
PutSession,
PutOption,
CommandOption,
PutArg,
} from 'koishi-thirdeye';
import { Game, ReportScoreResult } from './def/Match';
import { classToPlain, plainToClass } from 'class-transformer';
import _ from 'lodash';
export * from './config';
declare module 'koishi' {
interface Modules {
tabulate: typeof import('.');
}
namespace Cache {
interface Tables {
tabulateMatches: Record<string, Partial<Game>>;
}
}
}
@KoishiPlugin({ name: 'tabulate', schema: TabulatePluginConfig })
export default class TabulatePlugin implements OnApply {
constructor(private ctx: Context, config: TabulatePluginConfigLike) {}
async getGames(key: string) {
const dataFromCache = (await this.cache.get('tabulateMatches', key)) || {};
return _.mapValues(dataFromCache, (d) => plainToClass(Game, d));
}
async saveGame(game: Game) {
const dataFromCache =
(await this.cache.get('tabulateMatches', game.location)) || {};
dataFromCache[game.getKey()] = classToPlain(game);
return this.cache.set('tabulateMatches', game.location, dataFromCache);
}
async deleteGame(game: Game) {
const dataFromCache =
(await this.cache.get('tabulateMatches', game.location)) || {};
if (!dataFromCache[game.getKey()]) {
return;
}
delete dataFromCache[game.getKey()];
return this.cache.set('tabulateMatches', game.location, dataFromCache);
}
@InjectConfig()
private config: TabulatePluginConfig;
@Inject('cache')
private cache: Cache;
@UseEvent('service/cache')
loadCacheTable() {
this.cache.table('tabulateMatches', { maxAge: this.config.saveTime });
}
onApply() {
// patch tabulate specific rules
/*
this.ctx
.command('tabulate')
.option('rule', '--kof 使用 KOF 赛制', { value: '2/3【KOF】' })
.option('rule', '--head 使用人头赛赛制', { value: '人头赛' });
*/
}
@UseCommand('tabulate', '排表')
@CommandUsage('创建一个新的比赛,并进行排表操作。')
@CommandExample(
'tabulate --kof --teama AA --teamb BB --playera "Nanahira,Momobako,Miko" --playerb "Yuzurisa,Senya,Maika" 进行一个 KOF 对局的排表',
)
async onTabulate(
@PutSession() session: Session,
@PutOption('location', '-p <location:string> 比赛地点,默认为群内。')
location: string,
@PutOption('rule', '-r <rule:string> 比赛模式', { fallback: '2/3【KOF】' })
rule: string,
@PutOption('kof', '--kof 使用 KOF 赛制')
useKof: boolean,
@PutOption('head', '--head 使用 人头 赛制')
useHead: boolean,
@PutOption('teama', '--teama <teama:string> A 队队名', { fallback: 'A' })
teama: string,
@PutOption('teamb', '--teamb <teamb:string> B 队队名', { fallback: 'B' })
teamb: string,
@PutOption('playera', '--playera <playera:string> A 队队名,逗号分隔', {
fallback: 'a1,a2,a3',
})
playera: string,
@PutOption('playerb', '--playerb <playera:string> B 队队名,逗号分隔', {
fallback: 'b1,b2,b3',
})
playerb: string,
) {
const exactLocation =
location || session.guildId || session.channelId || session.userId;
if (!exactLocation) {
return '无法获取比赛地点,请使用 --location 参数手动指定。';
}
const parsedPlayerA = playera.split(',');
const parsedPlayerB = playerb.split(',');
if (
!parsedPlayerA.length ||
!parsedPlayerB.length ||
parsedPlayerA.length !== parsedPlayerB.length
) {
return '两队人数不一致,无法排表。';
}
const game = new Game(
exactLocation,
useKof ? '2/3【KOF】' : useHead ? '人头赛' : rule,
teama,
teamb,
parsedPlayerA,
parsedPlayerB,
);
await this.saveGame(game);
return game.format();
}
@UseCommand('tabulate/score <self:integer> <oppo:integer>', '上报比分')
@CommandUsage('进行比分的更新操作,并视情况进行下一轮或结束比赛。')
@CommandExample('score -n Nanahira 2 0 标记当局中 Nanahira 以 2:0 战胜对方。')
async updateScore(
@PutSession() session: Session,
@PutOption('location', '-p <location:string> 比赛地点,默认为群内。')
location: string,
@PutOption('name', '-n <name:string> 自己的 ID')
name: string,
@PutArg(0)
self: number,
@PutArg(1)
oppo: number,
) {
const exactLocation =
location || session.guildId || session.channelId || session.userId;
const possibleNames: string[] = _.uniq(
_.compact([
name,
session.author?.nickname,
session.author?.username,
session.username,
session.userId,
]),
);
const gamesMap = await this.getGames(exactLocation);
const game = Object.values(gamesMap).find((game) =>
game.includesPendingPlayer(possibleNames),
);
if (!game) {
return '未找到比赛。请尝试使用 -n <name> 指定自己的名称。';
}
const matchingName = game.includesPendingPlayer(possibleNames);
const result = game.reportScore(matchingName, self, oppo);
if (result === ReportScoreResult.NotFound) {
return `未找到对局。`;
}
if (result === ReportScoreResult.Finish) {
await this.deleteGame(game);
} else {
await this.saveGame(game);
}
return game.format();
}
}
{
"compilerOptions": {
"outDir": "dist",
"module": "commonjs",
"target": "es2021",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"declaration": true,
"sourceMap": true
},
"compileOnSave": true,
"allowJs": true,
"include": [
"*.ts",
"src/**/*.ts"
]
}
const path = require('path');
const packAll = !!process.env.PACK_ALL;
module.exports = {
entry: './src/index.ts',
mode: 'production',
target: 'node',
devtool: 'source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{ test: /\.mustache$/, use: 'raw-loader' },
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'index.js',
library: {
type: 'commonjs',
},
path: path.resolve(__dirname, packAll ? 'dist/full' : 'dist'),
},
externals: {
koishi: 'koishi',
...(packAll
? {}
: {
'koishi-thirdeye': 'koishi-thirdeye',
'class-transformer': 'class-transformer',
moment: 'moment',
lodash: 'lodash',
}),
},
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment