色々消し消し

This commit is contained in:
nullnyat 2022-09-18 00:15:51 +09:00
parent 44e25bb499
commit eb853be7e8
26 changed files with 0 additions and 2994 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
ai.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

14
ai.svg
View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="レイヤー-5" serif:id="レイヤー 5">
<path id="path4542" d="M125.972,500.997C122.6,507.74 115.708,512 108.169,512C85.271,512 36.32,512 12.944,512C10.172,512 7.597,510.564 6.139,508.206C4.681,505.847 4.549,502.902 5.789,500.422C32.536,446.929 144.098,223.803 173.55,164.899C174.906,162.189 177.676,160.477 180.706,160.477C183.736,160.477 186.506,162.189 187.861,164.899C217.313,223.803 328.876,446.929 355.623,500.422C356.863,502.902 356.73,505.847 355.273,508.206C353.815,510.564 351.24,512 348.468,512C325.092,512 276.141,512 253.243,512C245.704,512 238.811,507.74 235.44,500.997C224.508,479.134 200.061,430.239 187.884,405.885C186.524,403.166 183.745,401.449 180.706,401.449C177.666,401.449 174.888,403.166 173.528,405.885C161.351,430.239 136.904,479.134 125.972,500.997Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
<path id="path4544" d="M263.155,14.311C261.8,11.601 259.03,9.889 256,9.889C252.97,9.889 250.2,11.601 248.845,14.311C236.998,38.005 213.494,85.013 202.165,107.671C198.136,115.728 198.136,125.213 202.165,133.27C213.494,155.928 236.998,202.936 248.845,226.63C250.2,229.341 252.97,231.053 256,231.053C259.03,231.053 261.8,229.341 263.155,226.63C275.002,202.936 298.506,155.928 309.835,133.27C313.864,125.213 313.864,115.728 309.835,107.671C298.506,85.013 275.002,38.005 263.155,14.311Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
<path id="path4546" d="M506.211,500.422C507.451,502.902 507.319,505.847 505.861,508.206C504.403,510.564 501.828,512 499.056,512C476.392,512 429.685,512 405.993,512C397.129,512 389.025,506.992 385.061,499.064C364.356,457.653 299.8,328.542 278.189,285.32C273.701,276.342 273.701,265.775 278.189,256.798C289.771,233.634 312.541,188.095 324.139,164.899C325.494,162.189 328.264,160.477 331.294,160.477C334.324,160.477 337.094,162.189 338.45,164.899C367.902,223.803 479.465,446.929 506.211,500.422Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(361.412,0,0,361.412,1.88976e-05,331.294)"><stop offset="0" style="stop-color:rgb(71,116,158);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(192,139,174);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(316.235,0,0,512,195.765,256)"><stop offset="0" style="stop-color:rgb(71,116,158);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(202,217,201);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(316.235,0,0,512,195.765,256)"><stop offset="0" style="stop-color:rgb(71,116,158);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(202,217,201);stop-opacity:1"/></linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

489
src/ai.ts
View File

@ -1,489 +0,0 @@
// AI CORE
import * as fs from 'fs';
import autobind from 'autobind-decorator';
import * as loki from 'lokijs';
import * as request from 'request-promise-native';
import * as chalk from 'chalk';
import { v4 as uuid } from 'uuid';
const delay = require('timeout-as-promise');
import config from '@/config';
import Module from '@/module';
import Message from '@/message';
import Friend, { FriendDoc } from '@/friend';
import { User } from '@/misskey/user';
import Stream from '@/stream';
import log from '@/utils/log';
const pkg = require('../package.json');
type MentionHook = (msg: Message) => Promise<boolean | HandlerResult>;
type ContextHook = (key: any, msg: Message, data?: any) => Promise<void | boolean | HandlerResult>;
type TimeoutCallback = (data?: any) => void;
export type HandlerResult = {
reaction?: string | null;
immediate?: boolean;
};
export type InstallerResult = {
mentionHook?: MentionHook;
contextHook?: ContextHook;
timeoutCallback?: TimeoutCallback;
};
export type Meta = {
lastWakingAt: number;
};
/**
*
*/
export default class {
public readonly version = pkg._v;
public account: User;
public connection: Stream;
public modules: Module[] = [];
private mentionHooks: MentionHook[] = [];
private contextHooks: { [moduleName: string]: ContextHook } = {};
private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {};
public db: loki;
public lastSleepedAt: number;
private meta: loki.Collection<Meta>;
private contexts: loki.Collection<{
isDm: boolean;
noteId?: string;
userId?: string;
module: string;
key: string | null;
data?: any;
}>;
private timers: loki.Collection<{
id: string;
module: string;
insertedAt: number;
delay: number;
data?: any;
}>;
public friends: loki.Collection<FriendDoc>;
public moduleData: loki.Collection<any>;
/**
*
* @param account 使
* @param modules
*/
constructor(account: User, modules: Module[]) {
this.account = account;
this.modules = modules;
let memoryDir = '.';
if (config.memoryDir) {
memoryDir = config.memoryDir;
}
const file = process.env.NODE_ENV === 'test' ? `${memoryDir}/test.memory.json` : `${memoryDir}/memory.json`;
this.log(`Lodaing the memory from ${file}...`);
this.db = new loki(file, {
autoload: true,
autosave: true,
autosaveInterval: 1000,
autoloadCallback: err => {
if (err) {
this.log(chalk.red(`Failed to load the memory: ${err}`));
} else {
this.log(chalk.green('The memory loaded successfully'));
this.run();
}
}
});
}
@autobind
public log(msg: string) {
log(chalk`[{magenta AiOS}]: ${msg}`);
}
@autobind
private run() {
//#region Init DB
this.meta = this.getCollection('meta', {});
this.contexts = this.getCollection('contexts', {
indices: ['key']
});
this.timers = this.getCollection('timers', {
indices: ['module']
});
this.friends = this.getCollection('friends', {
indices: ['userId']
});
this.moduleData = this.getCollection('moduleData', {
indices: ['module']
});
//#endregion
const meta = this.getMeta();
this.lastSleepedAt = meta.lastWakingAt;
// Init stream
this.connection = new Stream();
//#region Main stream
const mainStream = this.connection.useSharedConnection('main');
// メンションされたとき
mainStream.on('mention', async data => {
if (data.userId == this.account.id) return; // 自分は弾く
if (data.text && data.text.startsWith('@' + this.account.username)) {
// Misskeyのバグで投稿が非公開扱いになる
if (data.text == null) data = await this.api('notes/show', { noteId: data.id });
this.onReceiveMessage(new Message(this, data, false));
}
});
// 返信されたとき
mainStream.on('reply', async data => {
if (data.userId == this.account.id) return; // 自分は弾く
if (data.text && data.text.startsWith('@' + this.account.username)) return;
// Misskeyのバグで投稿が非公開扱いになる
if (data.text == null) data = await this.api('notes/show', { noteId: data.id });
this.onReceiveMessage(new Message(this, data, false));
});
// Renoteされたとき
mainStream.on('renote', async data => {
if (data.userId == this.account.id) return; // 自分は弾く
if (data.text == null && (data.files || []).length == 0) return;
// リアクションする
this.api('notes/reactions/create', {
noteId: data.id,
reaction: 'love'
});
});
// メッセージ
mainStream.on('messagingMessage', data => {
if (data.userId == this.account.id) return; // 自分は弾く
this.onReceiveMessage(new Message(this, data, true));
});
// 通知
mainStream.on('notification', data => {
this.onNotification(data);
});
//#endregion
// Install modules
this.modules.forEach(m => {
this.log(`Installing ${chalk.cyan.italic(m.name)}\tmodule...`);
m.init(this);
const res = m.install();
if (res != null) {
if (res.mentionHook) this.mentionHooks.push(res.mentionHook);
if (res.contextHook) this.contextHooks[m.name] = res.contextHook;
if (res.timeoutCallback) this.timeoutCallbacks[m.name] = res.timeoutCallback;
}
});
// タイマー監視
this.crawleTimer();
setInterval(this.crawleTimer, 1000);
setInterval(this.logWaking, 10000);
this.log(chalk.green.bold('Ai am now running!'));
}
/**
*
* ()
*/
@autobind
private async onReceiveMessage(msg: Message): Promise<void> {
this.log(chalk.gray(`<<< An message received: ${chalk.underline(msg.id)}`));
// Ignore message if the user is a bot
// To avoid infinity reply loop.
if (msg.user.isBot) {
return;
}
const isNoContext = !msg.isDm && msg.replyId == null;
// Look up the context
const context = isNoContext ? null : this.contexts.findOne(msg.isDm ? {
isDm: true,
userId: msg.userId
} : {
isDm: false,
noteId: msg.replyId
});
let reaction: string | null = 'love';
let immediate: boolean = false;
//#region
const invokeMentionHooks = async () => {
let res: boolean | HandlerResult | null = null;
for (const handler of this.mentionHooks) {
res = await handler(msg);
if (res === true || typeof res === 'object') break;
}
if (res != null && typeof res === 'object') {
if (res.reaction != null) reaction = res.reaction;
if (res.immediate != null) immediate = res.immediate;
}
};
// コンテキストがあればコンテキストフック呼び出し
// なければそれぞれのモジュールについてフックが引っかかるまで呼び出し
if (context != null) {
const handler = this.contextHooks[context.module];
const res = await handler(context.key, msg, context.data);
if (res != null && typeof res === 'object') {
if (res.reaction != null) reaction = res.reaction;
if (res.immediate != null) immediate = res.immediate;
}
if (res === false) {
await invokeMentionHooks();
}
} else {
await invokeMentionHooks();
}
//#endregion
if (!immediate) {
await delay(1000);
}
if (msg.isDm) {
// 既読にする
this.api('messaging/messages/read', {
messageId: msg.id,
});
} else {
// リアクションする
if (reaction) {
this.api('notes/reactions/create', {
noteId: msg.id,
reaction: reaction
});
}
}
}
@autobind
private onNotification(notification: any) {
switch (notification.type) {
// リアクションされたら親愛度を少し上げる
// TODO: リアクション取り消しをよしなにハンドリングする
case 'reaction': {
const friend = new Friend(this, { user: notification.user });
friend.incLove(0.1);
break;
}
default:
break;
}
}
@autobind
private crawleTimer() {
const timers = this.timers.find();
for (const timer of timers) {
// タイマーが時間切れかどうか
if (Date.now() - (timer.insertedAt + timer.delay) >= 0) {
this.log(`Timer expired: ${timer.module} ${timer.id}`);
this.timers.remove(timer);
this.timeoutCallbacks[timer.module](timer.data);
}
}
}
@autobind
private logWaking() {
this.setMeta({
lastWakingAt: Date.now(),
});
}
/**
*
*/
@autobind
public getCollection(name: string, opts?: any): loki.Collection {
let collection: loki.Collection;
collection = this.db.getCollection(name);
if (collection == null) {
collection = this.db.addCollection(name, opts);
}
return collection;
}
@autobind
public lookupFriend(userId: User['id']): Friend | null {
const doc = this.friends.findOne({
userId: userId
});
if (doc == null) return null;
const friend = new Friend(this, { doc: doc });
return friend;
}
/**
*
*/
@autobind
public async upload(file: Buffer | fs.ReadStream, meta: any) {
const res = await request.post({
url: `${config.apiUrl}/drive/files/create`,
formData: {
i: config.i,
file: {
value: file,
options: meta
}
},
json: true
});
return res;
}
/**
* 稿
*/
@autobind
public async post(param: any) {
const res = await this.api('notes/create', param);
return res.createdNote;
}
/**
*
*/
@autobind
public sendMessage(userId: any, param: any) {
return this.api('messaging/messages/create', Object.assign({
userId: userId,
}, param));
}
/**
* APIを呼び出します
*/
@autobind
public api(endpoint: string, param?: any) {
return request.post(`${config.apiUrl}/${endpoint}`, {
json: Object.assign({
i: config.i
}, param)
});
};
/**
*
* @param module
* @param key
* @param isDm
* @param id ID稿ID
* @param data
*/
@autobind
public subscribeReply(module: Module, key: string | null, isDm: boolean, id: string, data?: any) {
this.contexts.insertOne(isDm ? {
isDm: true,
userId: id,
module: module.name,
key: key,
data: data
} : {
isDm: false,
noteId: id,
module: module.name,
key: key,
data: data
});
}
/**
*
* @param module
* @param key
*/
@autobind
public unsubscribeReply(module: Module, key: string | null) {
this.contexts.findAndRemove({
key: key,
module: module.name
});
}
/**
*
*
* @param module
* @param delay
* @param data
*/
@autobind
public setTimeoutWithPersistence(module: Module, delay: number, data?: any) {
const id = uuid();
this.timers.insertOne({
id: id,
module: module.name,
insertedAt: Date.now(),
delay: delay,
data: data
});
this.log(`Timer persisted: ${module.name} ${id} ${delay}ms`);
}
@autobind
public getMeta() {
const rec = this.meta.findOne();
if (rec) {
return rec;
} else {
const initial: Meta = {
lastWakingAt: Date.now(),
};
this.meta.insertOne(initial);
return initial;
}
}
@autobind
public setMeta(meta: Partial<Meta>) {
const rec = this.getMeta();
for (const [k, v] of Object.entries(meta)) {
rec[k] = v;
}
this.meta.update(rec);
}
}

View File

@ -1,160 +0,0 @@
import autobind from 'autobind-decorator';
import Module from '@/module';
import serifs from '@/serifs';
import Message from '@/message';
import { renderChart } from './render-chart';
import { items } from '@/vocabulary';
import config from '@/config';
export default class extends Module {
public readonly name = 'chart';
@autobind
public install() {
if (config.chartEnabled === false) return {};
this.post();
setInterval(this.post, 1000 * 60 * 3);
return {
mentionHook: this.mentionHook
};
}
@autobind
private async post() {
const now = new Date();
if (now.getHours() !== 23) return;
const date = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
const data = this.getData();
if (data.lastPosted == date) return;
data.lastPosted = date;
this.setData(data);
this.log('Time to chart');
const file = await this.genChart('notes');
this.log('Posting...');
this.ai.post({
text: serifs.chart.post,
fileIds: [file.id]
});
}
@autobind
private async genChart(type, params?): Promise<any> {
this.log('Chart data fetching...');
let chart;
if (type === 'userNotes') {
const data = await this.ai.api('charts/user/notes', {
span: 'day',
limit: 30,
userId: params.user.id
});
chart = {
title: `@${params.user.username}さんの投稿数`,
datasets: [{
data: data.diffs.normal
}, {
data: data.diffs.reply
}, {
data: data.diffs.renote
}]
};
} else if (type === 'followers') {
const data = await this.ai.api('charts/user/following', {
span: 'day',
limit: 30,
userId: params.user.id
});
chart = {
title: `@${params.user.username}さんのフォロワー数`,
datasets: [{
data: data.local.followers.total
}, {
data: data.remote.followers.total
}]
};
} else if (type === 'notes') {
const data = await this.ai.api('charts/notes', {
span: 'day',
limit: 30,
});
chart = {
datasets: [{
data: data.local.diffs.normal
}, {
data: data.local.diffs.reply
}, {
data: data.local.diffs.renote
}]
};
} else {
const suffixes = ['の売り上げ', 'の消費', 'の生産'];
const limit = 30;
const diffRange = 150;
const datasetCount = 1 + Math.floor(Math.random() * 3);
let datasets: any[] = [];
for (let d = 0; d < datasetCount; d++) {
let values = [Math.random() * 1000];
for (let i = 1; i < limit; i++) {
const prev = values[i - 1];
values.push(prev + ((Math.random() * (diffRange * 2)) - diffRange));
}
datasets.push({
data: values
});
}
chart = {
title: items[Math.floor(Math.random() * items.length)] + suffixes[Math.floor(Math.random() * suffixes.length)],
datasets: datasets
};
}
this.log('Chart rendering...');
const img = renderChart(chart);
this.log('Image uploading...');
const file = await this.ai.upload(img, {
filename: 'chart.png',
contentType: 'image/png'
});
return file;
}
@autobind
private async mentionHook(msg: Message) {
if (!msg.includes(['チャート'])) {
return false;
} else {
this.log('Chart requested');
}
let type = 'random';
if (msg.includes(['フォロワー'])) type = 'followers';
if (msg.includes(['投稿'])) type = 'userNotes';
const file = await this.genChart(type, {
user: msg.user
});
this.log('Replying...');
msg.reply(serifs.chart.foryou, { file });
return {
reaction: 'like'
};
}
}

View File

@ -1,215 +0,0 @@
import { createCanvas, registerFont } from 'canvas';
const width = 1024 + 256;
const height = 512 + 256;
const margin = 128;
const titleTextSize = 35;
const lineWidth = 16;
const yAxisThickness = 2;
const colors = {
bg: '#434343',
text: '#e0e4cc',
yAxis: '#5a5a5a',
dataset: [
'#ff4e50',
'#c2f725',
'#69d2e7',
'#f38630',
'#f9d423',
]
};
const yAxisTicks = 4;
type Chart = {
title?: string;
datasets: {
title?: string;
data: number[];
}[];
};
export function renderChart(chart: Chart) {
registerFont('./font.ttf', { family: 'CustomFont' });
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.antialias = 'default';
ctx.fillStyle = colors.bg;
ctx.beginPath();
ctx.fillRect(0, 0, width, height);
let chartAreaX = margin;
let chartAreaY = margin;
let chartAreaWidth = width - (margin * 2);
let chartAreaHeight = height - (margin * 2);
// Draw title
if (chart.title) {
ctx.font = `${titleTextSize}px CustomFont`;
const t = ctx.measureText(chart.title);
ctx.fillStyle = colors.text;
ctx.fillText(chart.title, (width / 2) - (t.width / 2), 128);
chartAreaY += titleTextSize;
chartAreaHeight -= titleTextSize;
}
const xAxisCount = chart.datasets[0].data.length;
const serieses = chart.datasets.length;
let lowerBound = Infinity;
let upperBound = -Infinity;
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
let v = 0;
for (let series = 0; series < serieses; series++) {
v += chart.datasets[series].data[xAxis];
}
if (v > upperBound) upperBound = v;
if (v < lowerBound) lowerBound = v;
}
// Calculate Y axis scale
const yAxisSteps = niceScale(lowerBound, upperBound, yAxisTicks);
const yAxisStepsMin = yAxisSteps[0];
const yAxisStepsMax = yAxisSteps[yAxisSteps.length - 1];
const yAxisRange = yAxisStepsMax - yAxisStepsMin;
// Draw Y axis
ctx.lineWidth = yAxisThickness;
ctx.lineCap = 'round';
ctx.strokeStyle = colors.yAxis;
for (let i = 0; i < yAxisSteps.length; i++) {
const step = yAxisSteps[yAxisSteps.length - i - 1];
const y = i * (chartAreaHeight / (yAxisSteps.length - 1));
ctx.beginPath();
ctx.lineTo(chartAreaX, chartAreaY + y);
ctx.lineTo(chartAreaX + chartAreaWidth, chartAreaY + y);
ctx.stroke();
ctx.font = '20px CustomFont';
ctx.fillStyle = colors.text;
ctx.fillText(step.toString(), chartAreaX, chartAreaY + y - 8);
}
const newDatasets: any[] = [];
for (let series = 0; series < serieses; series++) {
newDatasets.push({
data: []
});
}
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
for (let series = 0; series < serieses; series++) {
newDatasets[series].data.push(chart.datasets[series].data[xAxis] / yAxisRange);
}
}
const perXAxisWidth = chartAreaWidth / xAxisCount;
let newUpperBound = -Infinity;
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
let v = 0;
for (let series = 0; series < serieses; series++) {
v += newDatasets[series].data[xAxis];
}
if (v > newUpperBound) newUpperBound = v;
}
// Draw X axis
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
const xAxisPerTypeHeights: number[] = [];
for (let series = 0; series < serieses; series++) {
const v = newDatasets[series].data[xAxis];
const vHeight = (v / newUpperBound) * (chartAreaHeight - ((yAxisStepsMax - upperBound) / yAxisStepsMax * chartAreaHeight));
xAxisPerTypeHeights.push(vHeight);
}
for (let series = serieses - 1; series >= 0; series--) {
ctx.strokeStyle = colors.dataset[series % colors.dataset.length];
let total = 0;
for (let i = 0; i < series; i++) {
total += xAxisPerTypeHeights[i];
}
const height = xAxisPerTypeHeights[series];
const x = chartAreaX + (perXAxisWidth * ((xAxisCount - 1) - xAxis)) + (perXAxisWidth / 2);
const yTop = (chartAreaY + chartAreaHeight) - (total + height);
const yBottom = (chartAreaY + chartAreaHeight) - (total);
ctx.globalAlpha = 1 - (xAxis / xAxisCount);
ctx.beginPath();
ctx.lineTo(x, yTop);
ctx.lineTo(x, yBottom);
ctx.stroke();
}
}
return canvas.toBuffer();
}
// https://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axis
// https://github.com/apexcharts/apexcharts.js/blob/master/src/modules/Scales.js
// This routine creates the Y axis values for a graph.
function niceScale(lowerBound: number, upperBound: number, ticks: number): number[] {
if (lowerBound === 0 && upperBound === 0) return [0];
// Calculate Min amd Max graphical labels and graph
// increments. The number of ticks defaults to
// 10 which is the SUGGESTED value. Any tick value
// entered is used as a suggested value which is
// adjusted to be a 'pretty' value.
//
// Output will be an array of the Y axis values that
// encompass the Y values.
const steps: number[] = [];
// Determine Range
const range = upperBound - lowerBound;
let tiks = ticks + 1;
// Adjust ticks if needed
if (tiks < 2) {
tiks = 2;
} else if (tiks > 2) {
tiks -= 2;
}
// Get raw step value
const tempStep = range / tiks;
// Calculate pretty step value
const mag = Math.floor(Math.log10(tempStep));
const magPow = Math.pow(10, mag);
const magMsd = (parseInt as any)(tempStep / magPow);
const stepSize = magMsd * magPow;
// build Y label array.
// Lower and upper bounds calculations
const lb = stepSize * Math.floor(lowerBound / stepSize);
const ub = stepSize * Math.ceil(upperBound / stepSize);
// Build array
let val = lb;
while (1) {
steps.push(val);
val += stepSize;
if (val > ub) {
break;
}
}
return steps;
}

View File

@ -1,40 +0,0 @@
import autobind from 'autobind-decorator';
import Module from '@/module';
import Message from '@/message';
import serifs from '@/serifs';
export default class extends Module {
public readonly name = 'dice';
@autobind
public install() {
return {
mentionHook: this.mentionHook
};
}
@autobind
private async mentionHook(msg: Message) {
if (msg.text == null) return false;
const query = msg.text.match(/([0-9]+)[dD]([0-9]+)/);
if (query == null) return false;
const times = parseInt(query[1], 10);
const dice = parseInt(query[2], 10);
if (times < 1 || times > 10) return false;
if (dice < 2 || dice > 1000) return false;
const results: number[] = [];
for (let i = 0; i < times; i++) {
results.push(Math.floor(Math.random() * dice) + 1);
}
msg.reply(serifs.dice.done(results.join(' ')));
return true;
}
}

View File

@ -1,151 +0,0 @@
import autobind from 'autobind-decorator';
import Module from '@/module';
import Message from '@/message';
import serifs from '@/serifs';
const hands = [
'👏',
'👍',
'👎',
'👊',
'✊',
['🤛', '🤜'],
['🤜', '🤛'],
'🤞',
'✌',
'🤟',
'🤘',
'👌',
'👈',
'👉',
['👈', '👉'],
['👉', '👈'],
'👆',
'👇',
'☝',
['✋', '🤚'],
'🖐',
'🖖',
'👋',
'🤙',
'💪',
['💪', '✌'],
'🖕'
]
const faces = [
'😀',
'😃',
'😄',
'😁',
'😆',
'😅',
'😂',
'🤣',
'☺️',
'😊',
'😇',
'🙂',
'🙃',
'😉',
'😌',
'😍',
'🥰',
'😘',
'😗',
'😙',
'😚',
'😋',
'😛',
'😝',
'😜',
'🤪',
'🤨',
'🧐',
'🤓',
'😎',
'🤩',
'🥳',
'😏',
'😒',
'😞',
'😔',
'😟',
'😕',
'🙁',
'☹️',
'😣',
'😖',
'😫',
'😩',
'🥺',
'😢',
'😭',
'😤',
'😠',
'😡',
'🤬',
'🤯',
'😳',
'😱',
'😨',
'😰',
'😥',
'😓',
'🤗',
'🤔',
'🤭',
'🤫',
'🤥',
'😶',
'😐',
'😑',
'😬',
'🙄',
'😯',
'😦',
'😧',
'😮',
'😲',
'😴',
'🤤',
'😪',
'😵',
'🤐',
'🥴',
'🤢',
'🤮',
'🤧',
'😷',
'🤒',
'🤕',
'🤑',
'🤠',
'🗿',
'🤖',
'👽'
]
export default class extends Module {
public readonly name = 'emoji';
@autobind
public install() {
return {
mentionHook: this.mentionHook
};
}
@autobind
private async mentionHook(msg: Message) {
if (msg.includes(['顔文字', '絵文字', 'emoji', '福笑い'])) {
const hand = hands[Math.floor(Math.random() * hands.length)];
const face = faces[Math.floor(Math.random() * faces.length)];
const emoji = Array.isArray(hand) ? hand[0] + face + hand[1] : hand + face + hand;
msg.reply(serifs.emoji.suggest(emoji));
return true;
} else {
return false;
}
}
}

View File

@ -1,138 +0,0 @@
import autobind from 'autobind-decorator';
import * as loki from 'lokijs';
import Module from '@/module';
import Message from '@/message';
import serifs from '@/serifs';
export default class extends Module {
public readonly name = 'guessingGame';
private guesses: loki.Collection<{
userId: string;
secret: number;
tries: number[];
isEnded: boolean;
startedAt: number;
endedAt: number | null;
}>;
@autobind
public install() {
this.guesses = this.ai.getCollection('guessingGame', {
indices: ['userId']
});
return {
mentionHook: this.mentionHook,
contextHook: this.contextHook
};
}
@autobind
private async mentionHook(msg: Message) {
if (!msg.includes(['数当て', '数あて'])) return false;
const exist = this.guesses.findOne({
userId: msg.userId,
isEnded: false
});
if (!msg.isDm) {
if (exist != null) {
msg.reply(serifs.guessingGame.alreadyStarted);
} else {
msg.reply(serifs.guessingGame.plzDm);
}
return true;
}
const secret = Math.floor(Math.random() * 100);
this.guesses.insertOne({
userId: msg.userId,
secret: secret,
tries: [],
isEnded: false,
startedAt: Date.now(),
endedAt: null
});
msg.reply(serifs.guessingGame.started).then(reply => {
this.subscribeReply(msg.userId, msg.isDm, msg.isDm ? msg.userId : reply.id);
});
return true;
}
@autobind
private async contextHook(key: any, msg: Message) {
if (msg.text == null) return;
const exist = this.guesses.findOne({
userId: msg.userId,
isEnded: false
});
// 処理の流れ上、実際にnullになることは無さそうだけど一応
if (exist == null) {
this.unsubscribeReply(key);
return;
}
if (msg.text.includes('やめ')) {
msg.reply(serifs.guessingGame.cancel);
exist.isEnded = true;
exist.endedAt = Date.now();
this.guesses.update(exist);
this.unsubscribeReply(key);
return;
}
const guess = msg.extractedText.match(/[0-9]+/);
if (guess == null) {
msg.reply(serifs.guessingGame.nan).then(reply => {
this.subscribeReply(msg.userId, msg.isDm, reply.id);
});
return;
}
if (guess.length > 3) return;
const g = parseInt(guess[0], 10);
const firsttime = exist.tries.indexOf(g) === -1;
exist.tries.push(g);
let text: string;
let end = false;
if (exist.secret < g) {
text = firsttime
? serifs.guessingGame.less(g.toString())
: serifs.guessingGame.lessAgain(g.toString());
} else if (exist.secret > g) {
text = firsttime
? serifs.guessingGame.grater(g.toString())
: serifs.guessingGame.graterAgain(g.toString());
} else {
end = true;
text = serifs.guessingGame.congrats(exist.tries.length.toString());
}
if (end) {
exist.isEnded = true;
exist.endedAt = Date.now();
this.unsubscribeReply(key);
}
this.guesses.update(exist);
msg.reply(text).then(reply => {
if (!end) {
this.subscribeReply(msg.userId, msg.isDm, reply.id);
}
});
}
}

View File

@ -1,212 +0,0 @@
import autobind from 'autobind-decorator';
import * as loki from 'lokijs';
import Module from '@/module';
import Message from '@/message';
import serifs from '@/serifs';
import { User } from '@/misskey/user';
import { acct } from '@/utils/acct';
type Game = {
votes: {
user: {
id: string;
username: string;
host: User['host'];
};
number: number;
}[];
isEnded: boolean;
startedAt: number;
postId: string;
};
const limitMinutes = 10;
export default class extends Module {
public readonly name = 'kazutori';
private games: loki.Collection<Game>;
@autobind
public install() {
this.games = this.ai.getCollection('kazutori');
this.crawleGameEnd();
setInterval(this.crawleGameEnd, 1000);
return {
mentionHook: this.mentionHook,
contextHook: this.contextHook
};
}
@autobind
private async mentionHook(msg: Message) {
if (!msg.includes(['数取り'])) return false;
const games = this.games.find({});
const recentGame = games.length == 0 ? null : games[games.length - 1];
if (recentGame) {
// 現在アクティブなゲームがある場合
if (!recentGame.isEnded) {
msg.reply(serifs.kazutori.alreadyStarted, {
renote: recentGame.postId
});
return true;
}
// 直近のゲームから1時間経ってない場合
if (Date.now() - recentGame.startedAt < 1000 * 60 * 60) {
msg.reply(serifs.kazutori.matakondo);
return true;
}
}
const post = await this.ai.post({
text: serifs.kazutori.intro(limitMinutes)
});
this.games.insertOne({
votes: [],
isEnded: false,
startedAt: Date.now(),
postId: post.id
});
this.subscribeReply(null, false, post.id);
this.log('New kazutori game started');
return true;
}
@autobind
private async contextHook(key: any, msg: Message) {
if (msg.text == null) return {
reaction: 'hmm'
};
const game = this.games.findOne({
isEnded: false
});
// 処理の流れ上、実際にnullになることは無さそうだけど一応
if (game == null) return;
// 既に数字を取っていたら
if (game.votes.some(x => x.user.id == msg.userId)) return {
reaction: 'confused'
};
const match = msg.extractedText.match(/[0-9]+/);
if (match == null) return {
reaction: 'hmm'
};
const num = parseInt(match[0], 10);
// 整数じゃない
if (!Number.isInteger(num)) return {
reaction: 'hmm'
};
// 範囲外
if (num < 0 || num > 100) return {
reaction: 'confused'
};
this.log(`Voted ${num} by ${msg.user.id}`);
// 投票
game.votes.push({
user: {
id: msg.user.id,
username: msg.user.username,
host: msg.user.host
},
number: num
});
this.games.update(game);
return {
reaction: 'like'
};
}
/**
*
*/
@autobind
private crawleGameEnd() {
const game = this.games.findOne({
isEnded: false
});
if (game == null) return;
// 制限時間が経過していたら
if (Date.now() - game.startedAt >= 1000 * 60 * limitMinutes) {
this.finish(game);
}
}
/**
*
*/
@autobind
private finish(game: Game) {
game.isEnded = true;
this.games.update(game);
this.log('Kazutori game finished');
// お流れ
if (game.votes.length <= 1) {
this.ai.post({
text: serifs.kazutori.onagare,
renoteId: game.postId
});
return;
}
let results: string[] = [];
let winner: Game['votes'][0]['user'] | null = null;
for (let i = 100; i >= 0; i--) {
const users = game.votes
.filter(x => x.number == i)
.map(x => x.user);
if (users.length == 1) {
if (winner == null) {
winner = users[0];
const icon = i == 100 ? '💯' : '🎉';
results.push(`${icon} **${i}**: $[jelly ${acct(users[0])}]`);
} else {
results.push(` ${i}: ${acct(users[0])}`);
}
} else if (users.length > 1) {
results.push(`${i}: ${users.map(u => acct(u)).join(' ')}`);
}
}
const winnerFriend = winner ? this.ai.lookupFriend(winner.id) : null;
const name = winnerFriend ? winnerFriend.name : null;
const text = results.join('\n') + '\n\n' + (winner
? serifs.kazutori.finishWithWinner(acct(winner), name)
: serifs.kazutori.finishWithNoWinner);
this.ai.post({
text: text,
cw: serifs.kazutori.finish,
renoteId: game.postId
});
this.unsubscribeReply(null);
}
}

View File

@ -1,224 +0,0 @@
import * as gen from 'random-seed';
import { CellType } from './maze';
const cellVariants = {
void: {
digg: { left: null, right: null, top: null, bottom: null },
cross: { left: false, right: false, top: false, bottom: false },
},
empty: {
digg: { left: 'left', right: 'right', top: 'top', bottom: 'bottom' },
cross: { left: false, right: false, top: false, bottom: false },
},
left: {
digg: { left: null, right: 'leftRight', top: 'leftTop', bottom: 'leftBottom' },
cross: { left: false, right: false, top: false, bottom: false },
},
right: {
digg: { left: 'leftRight', right: null, top: 'rightTop', bottom: 'rightBottom' },
cross: { left: false, right: false, top: false, bottom: false },
},
top: {
digg: { left: 'leftTop', right: 'rightTop', top: null, bottom: 'topBottom' },
cross: { left: false, right: false, top: false, bottom: false },
},
bottom: {
digg: { left: 'leftBottom', right: 'rightBottom', top: 'topBottom', bottom: null },
cross: { left: false, right: false, top: false, bottom: false },
},
leftTop: {
digg: { left: null, right: 'leftRightTop', top: null, bottom: 'leftTopBottom' },
cross: { left: false, right: false, top: false, bottom: false },
},
leftBottom: {
digg: { left: null, right: 'leftRightBottom', top: 'leftTopBottom', bottom: null },
cross: { left: false, right: false, top: false, bottom: false },
},
rightTop: {
digg: { left: 'leftRightTop', right: null, top: null, bottom: 'rightTopBottom' },
cross: { left: false, right: false, top: false, bottom: false },
},
rightBottom: {
digg: { left: 'leftRightBottom', right: null, top: 'rightTopBottom', bottom: null },
cross: { left: false, right: false, top: false, bottom: false },
},
leftRightTop: {
digg: { left: null, right: null, top: null, bottom: null },
cross: { left: false, right: false, top: false, bottom: false },
},
leftRightBottom: {
digg: { left: null, right: null, top: null, bottom: null },
cross: { left: false, right: false, top: false, bottom: false },
},
leftTopBottom: {
digg: { left: null, right: null, top: null, bottom: null },
cross: { left: false, right: false, top: false, bottom: false },
},
rightTopBottom: {
digg: { left: null, right: null, top: null, bottom: null },
cross: { left: false, right: false, top: false, bottom: false },
},
leftRight: {
digg: { left: null, right: null, top: 'leftRightTop', bottom: 'leftRightBottom' },
cross: { left: false, right: false, top: true, bottom: true },
},
topBottom: {
digg: { left: 'leftTopBottom', right: 'rightTopBottom', top: null, bottom: null },
cross: { left: true, right: true, top: false, bottom: false },
},
cross: {
digg: { left: 'cross', right: 'cross', top: 'cross', bottom: 'cross' },
cross: { left: false, right: false, top: false, bottom: false },
},
} as { [k in CellType]: {
digg: { left: CellType | null; right: CellType | null; top: CellType | null; bottom: CellType | null; };
cross: { left: boolean; right: boolean; top: boolean; bottom: boolean; };
} };
type Dir = 'left' | 'right' | 'top' | 'bottom';
export function genMaze(seed, complexity?) {
const rand = gen.create(seed);
let mazeSize;
if (complexity) {
if (complexity === 'veryEasy') mazeSize = 3 + rand(3);
if (complexity === 'easy') mazeSize = 8 + rand(8);
if (complexity === 'hard') mazeSize = 22 + rand(13);
if (complexity === 'veryHard') mazeSize = 40 + rand(20);
if (complexity === 'ai') mazeSize = 100;
} else {
mazeSize = 11 + rand(21);
}
const donut = rand(3) === 0;
const donutWidth = 1 + Math.floor(mazeSize / 8) + rand(Math.floor(mazeSize / 4));
const straightMode = rand(3) === 0;
const straightness = 5 + rand(10);
// maze (filled by 'empty')
const maze: CellType[][] = new Array(mazeSize);
for (let i = 0; i < mazeSize; i++) {
maze[i] = new Array(mazeSize).fill('empty');
}
if (donut) {
for (let y = 0; y < mazeSize; y++) {
for (let x = 0; x < mazeSize; x++) {
if (x > donutWidth && x < (mazeSize - 1) - donutWidth && y > donutWidth && y < (mazeSize - 1) - donutWidth) {
maze[x][y] = 'void';
}
}
}
}
function checkDiggable(x: number, y: number, dir: Dir) {
if (cellVariants[maze[x][y]].digg[dir] === null) return false;
const newPos =
dir === 'top' ? { x: x, y: y - 1 } :
dir === 'bottom' ? { x: x, y: y + 1 } :
dir === 'left' ? { x: x - 1, y: y } :
dir === 'right' ? { x: x + 1, y: y } :
{ x, y };
if (newPos.x < 0 || newPos.y < 0 || newPos.x >= mazeSize || newPos.y >= mazeSize) return false;
const cell = maze[newPos.x][newPos.y];
if (cell === 'void') return false;
if (cell === 'empty') return true;
if (cellVariants[cell].cross[dir] && checkDiggable(newPos.x, newPos.y, dir)) return true;
return false;
}
function diggFrom(x: number, y: number, prevDir?: Dir) {
const isUpDiggable = checkDiggable(x, y, 'top');
const isRightDiggable = checkDiggable(x, y, 'right');
const isDownDiggable = checkDiggable(x, y, 'bottom');
const isLeftDiggable = checkDiggable(x, y, 'left');
if (!isUpDiggable && !isRightDiggable && !isDownDiggable && !isLeftDiggable) return;
const dirs: Dir[] = [];
if (isUpDiggable) dirs.push('top');
if (isRightDiggable) dirs.push('right');
if (isDownDiggable) dirs.push('bottom');
if (isLeftDiggable) dirs.push('left');
let dir: Dir;
if (straightMode && rand(straightness) !== 0) {
if (prevDir != null && dirs.includes(prevDir)) {
dir = prevDir;
} else {
dir = dirs[rand(dirs.length)];
}
} else {
dir = dirs[rand(dirs.length)];
}
maze[x][y] = cellVariants[maze[x][y]].digg[dir]!;
if (dir === 'top') {
maze[x][y - 1] = maze[x][y - 1] === 'empty' ? 'bottom' : 'cross';
diggFrom(x, y - 1, dir);
return;
}
if (dir === 'right') {
maze[x + 1][y] = maze[x + 1][y] === 'empty' ? 'left' : 'cross';
diggFrom(x + 1, y, dir);
return;
}
if (dir === 'bottom') {
maze[x][y + 1] = maze[x][y + 1] === 'empty' ? 'top' : 'cross';
diggFrom(x, y + 1, dir);
return;
}
if (dir === 'left') {
maze[x - 1][y] = maze[x - 1][y] === 'empty' ? 'right' : 'cross';
diggFrom(x - 1, y, dir);
return;
}
}
//#region start digg
const nonVoidCells: [number, number][] = [];
for (let y = 0; y < mazeSize; y++) {
for (let x = 0; x < mazeSize; x++) {
const cell = maze[x][y];
if (cell !== 'void') nonVoidCells.push([x, y]);
}
}
const origin = nonVoidCells[rand(nonVoidCells.length)];
diggFrom(origin[0], origin[1]);
//#endregion
let hasEmptyCell = true;
while (hasEmptyCell) {
const nonEmptyCells: [number, number][] = [];
for (let y = 0; y < mazeSize; y++) {
for (let x = 0; x < mazeSize; x++) {
const cell = maze[x][y];
if (cell !== 'empty' && cell !== 'void' && cell !== 'cross') nonEmptyCells.push([x, y]);
}
}
const pos = nonEmptyCells[rand(nonEmptyCells.length)];
diggFrom(pos[0], pos[1]);
hasEmptyCell = false;
for (let y = 0; y < mazeSize; y++) {
for (let x = 0; x < mazeSize; x++) {
if (maze[x][y] === 'empty') hasEmptyCell = true;
}
}
}
return maze;
}

View File

@ -1,80 +0,0 @@
import autobind from 'autobind-decorator';
import Module from '@/module';
import serifs from '@/serifs';
import { genMaze } from './gen-maze';
import { renderMaze } from './render-maze';
import Message from '@/message';
export default class extends Module {
public readonly name = 'maze';
@autobind
public install() {
this.post();
setInterval(this.post, 1000 * 60 * 3);
return {
mentionHook: this.mentionHook
};
}
@autobind
private async post() {
const now = new Date();
if (now.getHours() !== 22) return;
const date = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
const data = this.getData();
if (data.lastPosted == date) return;
data.lastPosted = date;
this.setData(data);
this.log('Time to maze');
const file = await this.genMazeFile(date);
this.log('Posting...');
this.ai.post({
text: serifs.maze.post,
fileIds: [file.id]
});
}
@autobind
private async genMazeFile(seed, size?): Promise<any> {
this.log('Maze generating...');
const maze = genMaze(seed, size);
this.log('Maze rendering...');
const data = renderMaze(seed, maze);
this.log('Image uploading...');
const file = await this.ai.upload(data, {
filename: 'maze.png',
contentType: 'image/png'
});
return file;
}
@autobind
private async mentionHook(msg: Message) {
if (msg.includes(['迷路'])) {
let size: string | null = null;
if (msg.includes(['接待'])) size = 'veryEasy';
if (msg.includes(['簡単', 'かんたん', '易しい', 'やさしい', '小さい', 'ちいさい'])) size = 'easy';
if (msg.includes(['難しい', 'むずかしい', '複雑な', '大きい', 'おおきい'])) size = 'hard';
if (msg.includes(['死', '鬼', '地獄'])) size = 'veryHard';
if (msg.includes(['藍']) && msg.includes(['本気'])) size = 'ai';
this.log('Maze requested');
setTimeout(async () => {
const file = await this.genMazeFile(Date.now(), size);
this.log('Replying...');
msg.reply(serifs.maze.foryou, { file });
}, 3000);
return {
reaction: 'like'
};
} else {
return false;
}
}
}

View File

@ -1 +0,0 @@
export type CellType = 'void' | 'empty' | 'left' | 'right' | 'top' | 'bottom' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom' | 'leftRightTop' | 'leftRightBottom' | 'leftTopBottom' | 'rightTopBottom' | 'leftRight' | 'topBottom' | 'cross';

View File

@ -1,243 +0,0 @@
import * as gen from 'random-seed';
import { createCanvas } from 'canvas';
import { CellType } from './maze';
import { themes } from './themes';
const imageSize = 4096; // px
const margin = 96 * 4;
const mazeAreaSize = imageSize - (margin * 2);
export function renderMaze(seed, maze: CellType[][]) {
const rand = gen.create(seed);
const mazeSize = maze.length;
const colors = themes[rand(themes.length)];
const canvas = createCanvas(imageSize, imageSize);
const ctx = canvas.getContext('2d');
ctx.antialias = 'none';
ctx.fillStyle = colors.bg1;
ctx.beginPath();
ctx.fillRect(0, 0, imageSize, imageSize);
ctx.fillStyle = colors.bg2;
ctx.beginPath();
ctx.fillRect(margin / 2, margin / 2, imageSize - ((margin / 2) * 2), imageSize - ((margin / 2) * 2));
// Draw
function drawCell(ctx, x, y, size, left, right, top, bottom, mark) {
const wallThickness = size / 6;
const margin = size / 6;
const markerMargin = size / 3;
ctx.fillStyle = colors.road;
if (left) {
ctx.beginPath();
ctx.fillRect(x, y + margin, size - margin, size - (margin * 2));
}
if (right) {
ctx.beginPath();
ctx.fillRect(x + margin, y + margin, size - margin, size - (margin * 2));
}
if (top) {
ctx.beginPath();
ctx.fillRect(x + margin, y, size - (margin * 2), size - margin);
}
if (bottom) {
ctx.beginPath();
ctx.fillRect(x + margin, y + margin, size - (margin * 2), size - margin);
}
if (mark) {
ctx.fillStyle = colors.marker;
ctx.beginPath();
ctx.fillRect(x + markerMargin, y + markerMargin, size - (markerMargin * 2), size - (markerMargin * 2));
}
ctx.strokeStyle = colors.wall;
ctx.lineWidth = wallThickness;
ctx.lineCap = 'square';
function line(ax, ay, bx, by) {
ctx.beginPath();
ctx.lineTo(x + ax, y + ay);
ctx.lineTo(x + bx, y + by);
ctx.stroke();
}
if (left && right && top && bottom) {
ctx.beginPath();
if (rand(2) === 0) {
line(0, margin, size, margin); // ─ 上
line(0, size - margin, size, size - margin); // ─ 下
line(margin, 0, margin, margin); // │ 左上
line(size - margin, 0, size - margin, margin); // │ 右上
line(margin, size - margin, margin, size); // │ 左下
line(size - margin, size - margin, size - margin, size); // │ 右下
} else {
line(margin, 0, margin, size); // │ 左
line(size - margin, 0, size - margin, size); // │ 右
line(0, margin, margin, margin); // ─ 左上
line(size - margin, margin, size, margin); // ─ 右上
line(0, size - margin, margin, size - margin); // ─ 左下
line(size - margin, size - margin, size, size - margin); // ─ 右下
}
return;
}
// ─
if (left && right && !top && !bottom) {
line(0, margin, size, margin); // ─ 上
line(0, size - margin, size, size - margin); // ─ 下
return;
}
// │
if (!left && !right && top && bottom) {
line(margin, 0, margin, size); // │ 左
line(size - margin, 0, size - margin, size); // │ 右
return;
}
// 左行き止まり
if (!left && right && !top && !bottom) {
line(margin, margin, size, margin); // ─ 上
line(margin, margin, margin, size - margin); // │ 左
line(margin, size - margin, size, size - margin); // ─ 下
return;
}
// 右行き止まり
if (left && !right && !top && !bottom) {
line(0, margin, size - margin, margin); // ─ 上
line(size - margin, margin, size - margin, size - margin); // │ 右
line(0, size - margin, size - margin, size - margin); // ─ 下
return;
}
// 上行き止まり
if (!left && !right && !top && bottom) {
line(margin, margin, size - margin, margin); // ─ 上
line(margin, margin, margin, size); // │ 左
line(size - margin, margin, size - margin, size); // │ 右
return;
}
// 下行き止まり
if (!left && !right && top && !bottom) {
line(margin, size - margin, size - margin, size - margin); // ─ 下
line(margin, 0, margin, size - margin); // │ 左
line(size - margin, 0, size - margin, size - margin); // │ 右
return;
}
// ┌
if (!left && !top && right && bottom) {
line(margin, margin, size, margin); // ─ 上
line(margin, margin, margin, size); // │ 左
line(size - margin, size - margin, size, size - margin); // ─ 下
line(size - margin, size - margin, size - margin, size); // │ 右
return;
}
// ┐
if (left && !right && !top && bottom) {
line(0, margin, size - margin, margin); // ─ 上
line(size - margin, margin, size - margin, size); // │ 右
line(0, size - margin, margin, size - margin); // ─ 下
line(margin, size - margin, margin, size); // │ 左
return;
}
// └
if (!left && right && top && !bottom) {
line(margin, 0, margin, size - margin); // │ 左
line(margin, size - margin, size, size - margin); // ─ 下
line(size - margin, 0, size - margin, margin); // │ 右
line(size - margin, margin, size, margin); // ─ 上
return;
}
// ┘
if (left && !right && top && !bottom) {
line(margin, 0, margin, margin); // │ 左
line(0, margin, margin, margin); // ─ 上
line(size - margin, 0, size - margin, size - margin); // │ 右
line(0, size - margin, size - margin, size - margin); // ─ 下
return;
}
// ├
if (!left && right && top && bottom) {
line(margin, 0, margin, size); // │ 左
line(size - margin, 0, size - margin, margin); // │ 右
line(size - margin, margin, size, margin); // ─ 上
line(size - margin, size - margin, size, size - margin); // ─ 下
line(size - margin, size - margin, size - margin, size); // │ 右
return;
}
// ┤
if (left && !right && top && bottom) {
line(size - margin, 0, size - margin, size); // │ 右
line(margin, 0, margin, margin); // │ 左
line(0, margin, margin, margin); // ─ 上
line(0, size - margin, margin, size - margin); // ─ 下
line(margin, size - margin, margin, size); // │ 左
return;
}
// ┬
if (left && right && !top && bottom) {
line(0, margin, size, margin); // ─ 上
line(0, size - margin, margin, size - margin); // ─ 下
line(margin, size - margin, margin, size); // │ 左
line(size - margin, size - margin, size, size - margin); // ─ 下
line(size - margin, size - margin, size - margin, size); // │ 右
return;
}
// ┴
if (left && right && top && !bottom) {
line(0, size - margin, size, size - margin); // ─ 下
line(margin, 0, margin, margin); // │ 左
line(0, margin, margin, margin); // ─ 上
line(size - margin, 0, size - margin, margin); // │ 右
line(size - margin, margin, size, margin); // ─ 上
return;
}
}
const cellSize = mazeAreaSize / mazeSize;
for (let x = 0; x < mazeSize; x++) {
for (let y = 0; y < mazeSize; y++) {
const actualX = margin + (cellSize * x);
const actualY = margin + (cellSize * y);
const cell = maze[x][y];
const mark = (x === 0 && y === 0) || (x === mazeSize - 1 && y === mazeSize - 1);
if (cell === 'left') drawCell(ctx, actualX, actualY, cellSize, true, false, false, false, mark);
if (cell === 'right') drawCell(ctx, actualX, actualY, cellSize, false, true, false, false, mark);
if (cell === 'top') drawCell(ctx, actualX, actualY, cellSize, false, false, true, false, mark);
if (cell === 'bottom') drawCell(ctx, actualX, actualY, cellSize, false, false, false, true, mark);
if (cell === 'leftTop') drawCell(ctx, actualX, actualY, cellSize, true, false, true, false, mark);
if (cell === 'leftBottom') drawCell(ctx, actualX, actualY, cellSize, true, false, false, true, mark);
if (cell === 'rightTop') drawCell(ctx, actualX, actualY, cellSize, false, true, true, false, mark);
if (cell === 'rightBottom') drawCell(ctx, actualX, actualY, cellSize, false, true, false, true, mark);
if (cell === 'leftRightTop') drawCell(ctx, actualX, actualY, cellSize, true, true, true, false, mark);
if (cell === 'leftRightBottom') drawCell(ctx, actualX, actualY, cellSize, true, true, false, true, mark);
if (cell === 'leftTopBottom') drawCell(ctx, actualX, actualY, cellSize, true, false, true, true, mark);
if (cell === 'rightTopBottom') drawCell(ctx, actualX, actualY, cellSize, false, true, true, true, mark);
if (cell === 'leftRight') drawCell(ctx, actualX, actualY, cellSize, true, true, false, false, mark);
if (cell === 'topBottom') drawCell(ctx, actualX, actualY, cellSize, false, false, true, true, mark);
if (cell === 'cross') drawCell(ctx, actualX, actualY, cellSize, true, true, true, true, mark);
}
}
return canvas.toBuffer();
}

View File

@ -1,55 +0,0 @@
export const themes = [{
bg1: '#C1D9CE',
bg2: '#F2EDD5',
wall: '#0F8AA6',
road: '#C1D9CE',
marker: '#84BFBF',
}, {
bg1: '#17275B',
bg2: '#1F2E67',
wall: '#17275B',
road: '#6A77A4',
marker: '#E6E5E3',
}, {
bg1: '#BFD962',
bg2: '#EAF2AC',
wall: '#1E4006',
road: '#BFD962',
marker: '#74A608',
}, {
bg1: '#C0CCB8',
bg2: '#FFE2C0',
wall: '#664A3C',
road: '#FFCB99',
marker: '#E78F72',
}, {
bg1: '#101010',
bg2: '#151515',
wall: '#909090',
road: '#202020',
marker: '#606060',
}, {
bg1: '#e0e0e0',
bg2: '#f2f2f2',
wall: '#a0a0a0',
road: '#e0e0e0',
marker: '#707070',
}, {
bg1: '#7DE395',
bg2: '#D0F3CF',
wall: '#349D9E',
road: '#7DE395',
marker: '#56C495',
}, {
bg1: '#C9EEEA',
bg2: '#DBF4F1',
wall: '#4BC6B9',
road: '#C9EEEA',
marker: '#19A89D',
}, {
bg1: '#1e231b',
bg2: '#27331e',
wall: '#67b231',
road: '#385622',
marker: '#78d337',
}];

View File

@ -1,146 +0,0 @@
import autobind from 'autobind-decorator';
import Message from '@/message';
import Module from '@/module';
import serifs from '@/serifs';
import { genItem } from '@/vocabulary';
import config from '@/config';
import { Note } from '@/misskey/note';
export default class extends Module {
public readonly name = 'poll';
@autobind
public install() {
setInterval(() => {
if (Math.random() < 0.1) {
this.post();
}
}, 1000 * 60 * 60);
return {
mentionHook: this.mentionHook,
timeoutCallback: this.timeoutCallback,
};
}
@autobind
private async post() {
const duration = 1000 * 60 * 15;
const polls = [ // TODO: Extract serif
['珍しそうなもの', 'みなさんは、どれがいちばん珍しいと思いますか?'],
['美味しそうなもの', 'みなさんは、どれがいちばん美味しいと思いますか?'],
['重そうなもの', 'みなさんは、どれがいちばん重いと思いますか?'],
['欲しいもの', 'みなさんは、どれがいちばん欲しいですか?'],
['無人島に持っていきたいもの', 'みなさんは、無人島にひとつ持っていけるとしたらどれにしますか?'],
['家に飾りたいもの', 'みなさんは、家に飾るとしたらどれにしますか?'],
['売れそうなもの', 'みなさんは、どれがいちばん売れそうだと思いますか?'],
['降ってきてほしいもの', 'みなさんは、どれが空から降ってきてほしいですか?'],
['携帯したいもの', 'みなさんは、どれを携帯したいですか?'],
['商品化したいもの', 'みなさんは、商品化するとしたらどれにしますか?'],
['発掘されそうなもの', 'みなさんは、遺跡から発掘されそうなものはどれだと思いますか?'],
['良い香りがしそうなもの', 'みなさんは、どれがいちばんいい香りがすると思いますか?'],
['高値で取引されそうなもの', 'みなさんは、どれがいちばん高値で取引されると思いますか?'],
['地球周回軌道上にありそうなもの', 'みなさんは、どれが地球周回軌道上を漂っていそうだと思いますか?'],
['プレゼントしたいもの', 'みなさんは、私にプレゼントしてくれるとしたらどれにしますか?'],
['プレゼントされたいもの', 'みなさんは、プレゼントでもらうとしたらどれにしますか?'],
['私が持ってそうなもの', 'みなさんは、私が持ってそうなものはどれだと思いますか?'],
['流行りそうなもの', 'みなさんは、どれが流行りそうだと思いますか?'],
['朝ごはん', 'みなさんは、朝ごはんにどれが食べたいですか?'],
['お昼ごはん', 'みなさんは、お昼ごはんにどれが食べたいですか?'],
['お夕飯', 'みなさんは、お夕飯にどれが食べたいですか?'],
['体に良さそうなもの', 'みなさんは、どれが体に良さそうだと思いますか?'],
['後世に遺したいもの', 'みなさんは、どれを後世に遺したいですか?'],
['楽器になりそうなもの', 'みなさんは、どれが楽器になりそうだと思いますか?'],
['お味噌汁の具にしたいもの', 'みなさんは、お味噌汁の具にするとしたらどれがいいですか?'],
['ふりかけにしたいもの', 'みなさんは、どれをごはんにふりかけたいですか?'],
['よく見かけるもの', 'みなさんは、どれをよく見かけますか?'],
['道に落ちてそうなもの', 'みなさんは、道端に落ちてそうなものはどれだと思いますか?'],
['美術館に置いてそうなもの', 'みなさんは、この中で美術館に置いてありそうなものはどれだと思いますか?'],
['教室にありそうなもの', 'みなさんは、教室にありそうなものってどれだと思いますか?'],
['絵文字になってほしいもの', '絵文字になってほしいものはどれですか?'],
['Misskey本部にありそうなもの', 'みなさんは、Misskey本部にありそうなものはどれだと思いますか'],
['燃えるゴミ', 'みなさんは、どれが燃えるゴミだと思いますか?'],
['好きなおにぎりの具', 'みなさんの好きなおにぎりの具はなんですか?'],
];
const poll = polls[Math.floor(Math.random() * polls.length)];
const choices = [
genItem(),
genItem(),
genItem(),
genItem(),
];
const note = await this.ai.post({
text: poll[1],
poll: {
choices,
expiredAfter: duration,
multiple: false,
}
});
// タイマーセット
this.setTimeoutWithPersistence(duration + 3000, {
title: poll[0],
noteId: note.id,
});
}
@autobind
private async mentionHook(msg: Message) {
if (!msg.or(['/poll']) || msg.user.username !== config.master) {
return false;
} else {
this.log('Manualy poll requested');
}
this.post();
return true;
}
@autobind
private async timeoutCallback({ title, noteId }) {
const note: Note = await this.ai.api('notes/show', { noteId });
const choices = note.poll!.choices;
let mostVotedChoice;
for (const choice of choices) {
if (mostVotedChoice == null) {
mostVotedChoice = choice;
continue;
}
if (choice.votes > mostVotedChoice.votes) {
mostVotedChoice = choice;
}
}
const mostVotedChoices = choices.filter(choice => choice.votes === mostVotedChoice.votes);
if (mostVotedChoice.votes === 0) {
this.ai.post({ // TODO: Extract serif
text: '投票はありませんでした',
renoteId: noteId,
});
} else if (mostVotedChoices.length === 1) {
this.ai.post({ // TODO: Extract serif
cw: `${title}アンケートの結果発表です!`,
text: `結果は${mostVotedChoice.votes}票の「${mostVotedChoice.text}」でした!`,
renoteId: noteId,
});
} else {
const choices = mostVotedChoices.map(choice => `${choice.text}`).join('と');
this.ai.post({ // TODO: Extract serif
cw: `${title}アンケートの結果発表です!`,
text: `結果は${mostVotedChoice.votes}票の${choices}でした!`,
renoteId: noteId,
});
}
}
}

View File

@ -1,451 +0,0 @@
/**
* -AI-
* Botのバックエンド()
*
*
*
*/
import 'module-alias/register';
import * as request from 'request-promise-native';
import Reversi, { Color } from 'misskey-reversi';
import config from '@/config';
import serifs from '@/serifs';
import { User } from '@/misskey/user';
function getUserName(user) {
return user.name || user.username;
}
const titles = [
'さん', 'サン', 'サン', '㌠',
'ちゃん', 'チャン', 'チャン',
'君', 'くん', 'クン', 'クン',
'先生', 'せんせい', 'センセイ', 'センセイ'
];
class Session {
private account: User;
private game: any;
private form: any;
private o: Reversi;
private botColor: Color;
/**
* ()
*/
private sumiNearIndexes: number[] = [];
/**
* ()
*/
private sumiIndexes: number[] = [];
/**
*
*/
private maxTurn;
/**
*
*/
private currentTurn = 0;
/**
* 稿
*/
private startedNote: any = null;
private get user(): User {
return this.game.user1Id == this.account.id ? this.game.user2 : this.game.user1;
}
private get userName(): string {
const name = getUserName(this.user);
return `?[${name}](${config.host}/@${this.user.username})${titles.some(x => name.endsWith(x)) ? '' : 'さん'}`;
}
private get strength(): number {
return this.form.find(i => i.id == 'strength').value;
}
private get isSettai(): boolean {
return this.strength === 0;
}
private get allowPost(): boolean {
return this.form.find(i => i.id == 'publish').value;
}
private get url(): string {
return `${config.host}/games/reversi/${this.game.id}`;
}
constructor() {
process.on('message', this.onMessage);
}
private onMessage = async (msg: any) => {
switch (msg.type) {
case '_init_': this.onInit(msg.body); break;
case 'updateForm': this.onUpdateForn(msg.body); break;
case 'started': this.onStarted(msg.body); break;
case 'ended': this.onEnded(msg.body); break;
case 'set': this.onSet(msg.body); break;
}
}
// 親プロセスからデータをもらう
private onInit = (msg: any) => {
this.game = msg.game;
this.form = msg.form;
this.account = msg.account;
}
/**
*
*/
private onUpdateForn = (msg: any) => {
this.form.find(i => i.id == msg.id).value = msg.value;
}
/**
*
*/
private onStarted = (msg: any) => {
this.game = msg;
// TLに投稿する
this.postGameStarted().then(note => {
this.startedNote = note;
});
// リバーシエンジン初期化
this.o = new Reversi(this.game.map, {
isLlotheo: this.game.isLlotheo,
canPutEverywhere: this.game.canPutEverywhere,
loopedBoard: this.game.loopedBoard
});
this.maxTurn = this.o.map.filter(p => p === 'empty').length - this.o.board.filter(x => x != null).length;
//#region 隅の位置計算など
//#region 隅
this.o.map.forEach((pix, i) => {
if (pix == 'null') return;
const [x, y] = this.o.transformPosToXy(i);
const get = (x, y) => {
if (x < 0 || y < 0 || x >= this.o.mapWidth || y >= this.o.mapHeight) return 'null';
return this.o.mapDataGet(this.o.transformXyToPos(x, y));
};
const isNotSumi = (
// -
// +
// -
(get(x - 1, y - 1) == 'empty' && get(x + 1, y + 1) == 'empty') ||
// -
// +
// -
(get(x, y - 1) == 'empty' && get(x, y + 1) == 'empty') ||
// -
// +
// -
(get(x + 1, y - 1) == 'empty' && get(x - 1, y + 1) == 'empty') ||
//
// -+-
//
(get(x - 1, y) == 'empty' && get(x + 1, y) == 'empty')
)
const isSumi = !isNotSumi;
if (isSumi) this.sumiIndexes.push(i);
});
//#endregion
//#region 隅の隣
this.o.map.forEach((pix, i) => {
if (pix == 'null') return;
if (this.sumiIndexes.includes(i)) return;
const [x, y] = this.o.transformPosToXy(i);
const check = (x, y) => {
if (x < 0 || y < 0 || x >= this.o.mapWidth || y >= this.o.mapHeight) return 0;
return this.sumiIndexes.includes(this.o.transformXyToPos(x, y));
};
const isSumiNear = (
check(x - 1, y - 1) || // 左上
check(x , y - 1) || // 上
check(x + 1, y - 1) || // 右上
check(x + 1, y ) || // 右
check(x + 1, y + 1) || // 右下
check(x , y + 1) || // 下
check(x - 1, y + 1) || // 左下
check(x - 1, y ) // 左
)
if (isSumiNear) this.sumiNearIndexes.push(i);
});
//#endregion
//#endregion
this.botColor = this.game.user1Id == this.account.id && this.game.black == 1 || this.game.user2Id == this.account.id && this.game.black == 2;
if (this.botColor) {
this.think();
}
}
/**
*
*/
private onEnded = async (msg: any) => {
// ストリームから切断
process.send!({
type: 'ended'
});
let text: string;
if (msg.game.surrendered) {
if (this.isSettai) {
text = serifs.reversi.settaiButYouSurrendered(this.userName);
} else {
text = serifs.reversi.youSurrendered(this.userName);
}
} else if (msg.winnerId) {
if (msg.winnerId == this.account.id) {
if (this.isSettai) {
text = serifs.reversi.iWonButSettai(this.userName);
} else {
text = serifs.reversi.iWon(this.userName);
}
} else {
if (this.isSettai) {
text = serifs.reversi.iLoseButSettai(this.userName);
} else {
text = serifs.reversi.iLose(this.userName);
}
}
} else {
if (this.isSettai) {
text = serifs.reversi.drawnSettai(this.userName);
} else {
text = serifs.reversi.drawn(this.userName);
}
}
await this.post(text, this.startedNote);
process.exit();
}
/**
*
*/
private onSet = (msg: any) => {
this.o.put(msg.color, msg.pos);
this.currentTurn++;
if (msg.next === this.botColor) {
this.think();
}
}
/**
* Botにとってある局面がどれだけ有利か静的に評価する
* static()
* TODO: 接待時はまるっと処理の中身を変え
*/
private staticEval = () => {
let score = this.o.canPutSomewhere(this.botColor).length;
for (const index of this.sumiIndexes) {
const stone = this.o.board[index];
if (stone === this.botColor) {
score += 1000; // 自分が隅を取っていたらスコアプラス
} else if (stone !== null) {
score -= 1000; // 相手が隅を取っていたらスコアマイナス
}
}
// TODO: ここに (隅以外の確定石の数 * 100) をスコアに加算する処理を入れる
for (const index of this.sumiNearIndexes) {
const stone = this.o.board[index];
if (stone === this.botColor) {
score -= 10; // 自分が隅の周辺を取っていたらスコアマイナス(危険なので)
} else if (stone !== null) {
score += 10; // 相手が隅の周辺を取っていたらスコアプラス
}
}
// ロセオならスコアを反転
if (this.game.isLlotheo) score = -score;
// 接待ならスコアを反転
if (this.isSettai) score = -score;
return score;
}
private think = () => {
console.log(`(${this.currentTurn}/${this.maxTurn}) Thinking...`);
console.time('think');
// 接待モードのときは、全力(5手先読みくらい)で負けるようにする
// TODO: 接待のときは、どちらかというと「自分が不利になる手を選ぶ」というよりは、「相手に角を取らせられる手を選ぶ」ように思考する
// 自分が不利になる手を選ぶというのは、換言すれば自分が打てる箇所を減らすことになるので、
// 自分が打てる箇所が少ないと結果的に思考の選択肢が狭まり、対局をコントロールするのが難しくなるジレンマのようなものがある。
// つまり「相手を勝たせる」という意味での正しい接待は、「ゲーム序盤・中盤までは(通常通り)自分の有利になる手を打ち、終盤になってから相手が勝つように打つ」こと。
// とはいえ藍に求められているのは、そういった「本物の」接待ではなく、単に「角を取らせてくれる」接待だと思われるので、
// 静的評価で「角に相手の石があるかどうか(と、ゲームが終わったときは相手が勝っているかどうか)」を考慮するようにすれば良いかもしれない。
const maxDepth = this.isSettai ? 5 : this.strength;
/**
* αβ
*/
const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
// 試し打ち
this.o.put(this.o.turn, pos);
const isBotTurn = this.o.turn === this.botColor;
// 勝った
if (this.o.turn === null) {
const winner = this.o.winner;
// 勝つことによる基本スコア
const base = 10000;
let score;
if (this.game.isLlotheo) {
// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
score = this.o.winner ? base - (this.o.blackCount * 100) : base - (this.o.whiteCount * 100);
} else {
// 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する
score = this.o.winner ? base + (this.o.blackCount * 100) : base + (this.o.whiteCount * 100);
}
// 巻き戻し
this.o.undo();
// 接待なら自分が負けた方が高スコア
return this.isSettai
? winner !== this.botColor ? score : -score
: winner === this.botColor ? score : -score;
}
if (depth === maxDepth) {
// 静的に評価
const score = this.staticEval();
// 巻き戻し
this.o.undo();
return score;
} else {
const cans = this.o.canPutSomewhere(this.o.turn);
let value = isBotTurn ? -Infinity : Infinity;
let a = alpha;
let b = beta;
// TODO: 残りターン数というよりも「空いているマスが12以下」の場合に完全読みさせる
const nextDepth = (this.strength >= 4) && ((this.maxTurn - this.currentTurn) <= 12) ? Infinity : depth + 1;
// 次のターンのプレイヤーにとって最も良い手を取得
// TODO: cansをまず浅く読んで(または価値マップを利用して)から有益そうな手から順に並べ替え、効率よく枝刈りできるようにする
for (const p of cans) {
if (isBotTurn) {
const score = dive(p, a, beta, nextDepth);
value = Math.max(value, score);
a = Math.max(a, value);
if (value >= beta) break;
} else {
const score = dive(p, alpha, b, nextDepth);
value = Math.min(value, score);
b = Math.min(b, value);
if (value <= alpha) break;
}
}
// 巻き戻し
this.o.undo();
return value;
}
};
const cans = this.o.canPutSomewhere(this.botColor);
const scores = cans.map(p => dive(p));
const pos = cans[scores.indexOf(Math.max(...scores))];
console.log('Thinked:', pos);
console.timeEnd('think');
setTimeout(() => {
process.send!({
type: 'put',
pos
});
}, 500);
}
/**
* Misskeyに投稿します
*/
private postGameStarted = async () => {
const text = this.isSettai
? serifs.reversi.startedSettai(this.userName)
: serifs.reversi.started(this.userName, this.strength.toString());
return await this.post(`${text}\n→[観戦する](${this.url})`);
}
/**
* Misskeyに投稿します
* @param text 稿
*/
private post = async (text: string, renote?: any) => {
if (this.allowPost) {
const body = {
i: config.i,
text: text,
visibility: 'home'
} as any;
if (renote) {
body.renoteId = renote.id;
}
try {
const res = await request.post(`${config.host}/api/notes/create`, {
json: body
});
return res.createdNote;
} catch (e) {
console.error(e);
return null;
}
} else {
return null;
}
}
}
new Session();

View File

@ -1,180 +0,0 @@
import * as childProcess from 'child_process';
import autobind from 'autobind-decorator';
import Module from '@/module';
import serifs from '@/serifs';
import config from '@/config';
import Message from '@/message';
import Friend from '@/friend';
import getDate from '@/utils/get-date';
export default class extends Module {
public readonly name = 'reversi';
/**
*
*/
private reversiConnection?: any;
@autobind
public install() {
if (!config.reversiEnabled) return {};
this.reversiConnection = this.ai.connection.useSharedConnection('gamesReversi');
// 招待されたとき
this.reversiConnection.on('invited', msg => this.onReversiInviteMe(msg.parent));
// マッチしたとき
this.reversiConnection.on('matched', msg => this.onReversiGameStart(msg));
if (config.reversiEnabled) {
const mainStream = this.ai.connection.useSharedConnection('main');
mainStream.on('pageEvent', msg => {
if (msg.event === 'inviteReversi') {
this.ai.api('games/reversi/match', {
userId: msg.user.id
});
}
});
}
return {
mentionHook: this.mentionHook
};
}
@autobind
private async mentionHook(msg: Message) {
if (msg.includes(['リバーシ', 'オセロ', 'reversi', 'othello'])) {
if (config.reversiEnabled) {
msg.reply(serifs.reversi.ok);
this.ai.api('games/reversi/match', {
userId: msg.userId
});
} else {
msg.reply(serifs.reversi.decline);
}
return true;
} else {
return false;
}
}
@autobind
private async onReversiInviteMe(inviter: any) {
this.log(`Someone invited me: @${inviter.username}`);
if (config.reversiEnabled) {
// 承認
const game = await this.ai.api('games/reversi/match', {
userId: inviter.id
});
this.onReversiGameStart(game);
} else {
// todo (リバーシできない旨をメッセージで伝えるなど)
}
}
@autobind
private onReversiGameStart(game: any) {
this.log('enter reversi game room');
// ゲームストリームに接続
const gw = this.ai.connection.connectToChannel('gamesReversiGame', {
gameId: game.id
});
// フォーム
const form = [{
id: 'publish',
type: 'switch',
label: '藍が対局情報を投稿するのを許可',
value: true
}, {
id: 'strength',
type: 'radio',
label: '強さ',
value: 3,
items: [{
label: '接待',
value: 0
}, {
label: '弱',
value: 2
}, {
label: '中',
value: 3
}, {
label: '強',
value: 4
}, {
label: '最強',
value: 5
}]
}];
//#region バックエンドプロセス開始
const ai = childProcess.fork(__dirname + '/back.js');
// バックエンドプロセスに情報を渡す
ai.send({
type: '_init_',
body: {
game: game,
form: form,
account: this.ai.account
}
});
ai.on('message', (msg: Record<string, any>) => {
if (msg.type == 'put') {
gw.send('set', {
pos: msg.pos
});
} else if (msg.type == 'ended') {
gw.dispose();
this.onGameEnded(game);
}
});
// ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える
gw.addListener('*', message => {
ai.send(message);
});
//#endregion
// フォーム初期化
setTimeout(() => {
gw.send('initForm', form);
}, 1000);
// どんな設定内容の対局でも受け入れる
setTimeout(() => {
gw.send('accept', {});
}, 2000);
}
@autobind
private onGameEnded(game: any) {
const user = game.user1Id == this.ai.account.id ? game.user2 : game.user1;
//#region 1日に1回だけ親愛度を上げる
const today = getDate();
const friend = new Friend(this.ai, { user: user });
const data = friend.getPerModulesData(this);
if (data.lastPlayedAt != today) {
data.lastPlayedAt = today;
friend.setPerModulesData(this, data);
friend.incLove();
}
//#endregion
}
}

View File

@ -1,33 +0,0 @@
import autobind from 'autobind-decorator';
import Module from '@/module';
export default class extends Module {
public readonly name = 'welcome';
@autobind
public install() {
const tl = this.ai.connection.useSharedConnection('localTimeline');
tl.on('note', this.onLocalNote);
return {};
}
@autobind
private onLocalNote(note: any) {
if (note.isFirstNote) {
setTimeout(() => {
this.ai.api('notes/create', {
renoteId: note.id
});
}, 3000);
setTimeout(() => {
this.ai.api('notes/reactions/create', {
noteId: note.id,
reaction: 'congrats'
});
}, 5000);
}
}
}

View File

@ -1,10 +0,0 @@
import { katakanaToHiragana, hankakuToZenkaku } from './japanese';
export default function(text: string, words: string[]): boolean {
if (text == null) return false;
text = katakanaToHiragana(hankakuToZenkaku(text)).toLowerCase();
words = words.map(word => katakanaToHiragana(word).toLowerCase());
return words.some(word => text.includes(word));
}

View File

@ -1,7 +0,0 @@
export const account = {
id: '0',
name: '藍',
username: 'ai',
host: null,
isBot: true,
};

View File

@ -1,67 +0,0 @@
import * as http from 'http';
import * as Koa from 'koa';
import * as websocket from 'websocket';
export class Misskey {
private server: http.Server;
private streaming: websocket.connection;
constructor() {
const app = new Koa();
this.server = http.createServer(app.callback());
const ws = new websocket.server({
httpServer: this.server
});
ws.on('request', async (request) => {
const q = request.resourceURL.query as ParsedUrlQuery;
this.streaming = request.accept();
});
this.server.listen(3000);
}
public waitForStreamingMessage(handler) {
return new Promise((resolve, reject) => {
const onMessage = (data: websocket.IMessage) => {
if (data.utf8Data == null) return;
const message = JSON.parse(data.utf8Data);
const result = handler(message);
if (result) {
this.streaming.off('message', onMessage);
resolve();
}
};
this.streaming.on('message', onMessage);
});
}
public async waitForMainChannelConnected() {
await this.waitForStreamingMessage(message => {
const { type, body } = message;
if (type === 'connect') {
const { channel, id, params, pong } = body;
if (channel !== 'main') return;
if (pong) {
this.sendStreamingMessage('connected', {
id: id
});
}
return true;
}
});
}
public sendStreamingMessage(type: string, payload: any) {
this.streaming.send(JSON.stringify({
type: type,
body: payload
}));
}
}

View File

@ -1,17 +0,0 @@
import * as websocket from 'websocket';
export class StreamingApi {
private ws: WS;
constructor() {
this.ws = new WS('ws://localhost/streaming');
}
public async waitForMainChannelConnected() {
await expect(this.ws).toReceiveMessage("hello");
}
public send(message) {
this.ws.send(JSON.stringify(message));
}
}

View File

@ -1,26 +0,0 @@
import autobind from 'autobind-decorator';
import Module from '@/module';
import Message from '@/message';
export default class extends Module {
public readonly name = 'test';
@autobind
public install() {
return {
mentionHook: this.mentionHook
};
}
@autobind
private async mentionHook(msg: Message) {
if (msg.text && msg.text.includes('ping')) {
msg.reply('PONG!', {
immediate: true
});
return true;
} else {
return false;
}
}
}

View File

@ -1,20 +0,0 @@
import from '@/ai';
import { account } from '#/__mocks__/account';
import TestModule from '#/__modules__/test';
import { StreamingApi } from '#/__mocks__/ws';
process.env.NODE_ENV = 'test';
let ai: ;
beforeEach(() => {
ai = new (account, [
new TestModule(),
]);
});
test('mention hook', async () => {
const streaming = new StreamingApi();
});

View File

@ -1,15 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "../",
"paths": {
"@/*": ["../src/*"],
"#/*": ["./*"]
},
},
"compileOnSave": false,
"include": [
"**/*.ts"
]
}