This commit is contained in:
syuilo 2018-04-02 04:15:27 +09:00
parent e8bde94e5b
commit cd2542e0fd
99 changed files with 111 additions and 111 deletions

View file

@ -0,0 +1,27 @@
import getPostSummary from './get-post-summary';
import getReactionEmoji from './get-reaction-emoji';
/**
*
* @param notification
*/
export default function(notification: any): string {
switch (notification.type) {
case 'follow':
return `${notification.user.name}にフォローされました`;
case 'mention':
return `言及されました:\n${notification.user.name}${getPostSummary(notification.post)}`;
case 'reply':
return `返信されました:\n${notification.user.name}${getPostSummary(notification.post)}`;
case 'repost':
return `Repostされました:\n${notification.user.name}${getPostSummary(notification.post)}`;
case 'quote':
return `引用されました:\n${notification.user.name}${getPostSummary(notification.post)}`;
case 'reaction':
return `リアクションされました:\n${notification.user.name} <${getReactionEmoji(notification.reaction)}>「${getPostSummary(notification.post)}`;
case 'poll_vote':
return `投票されました:\n${notification.user.name}${getPostSummary(notification.post)}`;
default:
return `<不明な通知タイプ: ${notification.type}>`;
}
}

View file

@ -0,0 +1,45 @@
/**
* 稿
* @param {*} post 稿
*/
const summarize = (post: any): string => {
let summary = '';
// チャンネル
summary += post.channel ? `${post.channel.title}:` : '';
// 本文
summary += post.text ? post.text : '';
// メディアが添付されているとき
if (post.media) {
summary += ` (${post.media.length}つのメディア)`;
}
// 投票が添付されているとき
if (post.poll) {
summary += ' (投票)';
}
// 返信のとき
if (post.replyId) {
if (post.reply) {
summary += ` RE: ${summarize(post.reply)}`;
} else {
summary += ' RE: ...';
}
}
// Repostのとき
if (post.repostId) {
if (post.repost) {
summary += ` RP: ${summarize(post.repost)}`;
} else {
summary += ' RP: ...';
}
}
return summary.trim();
};
export default summarize;

View file

@ -0,0 +1,14 @@
export default function(reaction: string): string {
switch (reaction) {
case 'like': return '👍';
case 'love': return '❤️';
case 'laugh': return '😆';
case 'hmm': return '🤔';
case 'surprise': return '😮';
case 'congrats': return '🎉';
case 'angry': return '💢';
case 'confused': return '😥';
case 'pudding': return '🍮';
default: return '';
}
}

376
src/misc/othello/ai/back.ts Normal file
View file

@ -0,0 +1,376 @@
/**
* -AI-
* Botのバックエンド()
*
*
*
*/
import * as request from 'request-promise-native';
import Othello, { Color } from '../core';
import conf from '../../../conf';
let game;
let form;
/**
* BotアカウントのユーザーID
*/
const id = conf.othello_ai.id;
/**
* BotアカウントのAPIキー
*/
const i = conf.othello_ai.i;
let post;
process.on('message', async msg => {
// 親プロセスからデータをもらう
if (msg.type == '_init_') {
game = msg.game;
form = msg.form;
}
// フォームが更新されたとき
if (msg.type == 'update-form') {
form.find(i => i.id == msg.body.id).value = msg.body.value;
}
// ゲームが始まったとき
if (msg.type == 'started') {
onGameStarted(msg.body);
//#region TLに投稿する
const game = msg.body;
const url = `${conf.url}/othello/${game.id}`;
const user = game.user1Id == id ? game.user2 : game.user1;
const isSettai = form[0].value === 0;
const text = isSettai
? `?[${user.name}](${conf.url}/@${user.username})さんの接待を始めました!`
: `対局を?[${user.name}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`;
const res = await request.post(`${conf.api_url}/posts/create`, {
json: { i,
text: `${text}\n→[観戦する](${url})`
}
});
post = res.createdPost;
//#endregion
}
// ゲームが終了したとき
if (msg.type == 'ended') {
// ストリームから切断
process.send({
type: 'close'
});
//#region TLに投稿する
const user = game.user1Id == id ? game.user2 : game.user1;
const isSettai = form[0].value === 0;
const text = isSettai
? msg.body.winnerId === null
? `?[${user.name}](${conf.url}/@${user.username})さんに接待で引き分けました...`
: msg.body.winnerId == id
? `?[${user.name}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...`
: `?[${user.name}](${conf.url}/@${user.username})さんに接待で負けてあげました♪`
: msg.body.winnerId === null
? `?[${user.name}](${conf.url}/@${user.username})さんと引き分けました~`
: msg.body.winnerId == id
? `?[${user.name}](${conf.url}/@${user.username})さんに勝ちました♪`
: `?[${user.name}](${conf.url}/@${user.username})さんに負けました...`;
await request.post(`${conf.api_url}/posts/create`, {
json: { i,
repostId: post.id,
text: text
}
});
//#endregion
process.exit();
}
// 打たれたとき
if (msg.type == 'set') {
onSet(msg.body);
}
});
let o: Othello;
let botColor: Color;
// 各マスの強さ
let cellWeights;
/**
*
* @param g
*/
function onGameStarted(g) {
game = g;
// オセロエンジン初期化
o = new Othello(game.settings.map, {
isLlotheo: game.settings.isLlotheo,
canPutEverywhere: game.settings.canPutEverywhere,
loopedBoard: game.settings.loopedBoard
});
// 各マスの価値を計算しておく
cellWeights = o.map.map((pix, i) => {
if (pix == 'null') return 0;
const [x, y] = o.transformPosToXy(i);
let count = 0;
const get = (x, y) => {
if (x < 0 || y < 0 || x >= o.mapWidth || y >= o.mapHeight) return 'null';
return o.mapDataGet(o.transformXyToPos(x, y));
};
if (get(x , y - 1) == 'null') count++;
if (get(x + 1, y - 1) == 'null') count++;
if (get(x + 1, y ) == 'null') count++;
if (get(x + 1, y + 1) == 'null') count++;
if (get(x , y + 1) == 'null') count++;
if (get(x - 1, y + 1) == 'null') count++;
if (get(x - 1, y ) == 'null') count++;
if (get(x - 1, y - 1) == 'null') count++;
//return Math.pow(count, 3);
return count >= 4 ? 1 : 0;
});
botColor = game.user1Id == id && game.black == 1 || game.user2Id == id && game.black == 2;
if (botColor) {
think();
}
}
function onSet(x) {
o.put(x.color, x.pos);
if (x.next === botColor) {
think();
}
}
const db = {};
function think() {
console.log('Thinking...');
console.time('think');
const isSettai = form[0].value === 0;
// 接待モードのときは、全力(5手先読みくらい)で負けるようにする
const maxDepth = isSettai ? 5 : form[0].value;
/**
* Botにとってある局面がどれだけ有利か取得する
*/
function staticEval() {
let score = o.canPutSomewhere(botColor).length;
cellWeights.forEach((weight, i) => {
// 係数
const coefficient = 30;
weight = weight * coefficient;
const stone = o.board[i];
if (stone === botColor) {
// TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する
score += weight;
} else if (stone !== null) {
score -= weight;
}
});
// ロセオならスコアを反転
if (game.settings.isLlotheo) score = -score;
// 接待ならスコアを反転
if (isSettai) score = -score;
return score;
}
/**
* αβ
*/
const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
// 試し打ち
o.put(o.turn, pos);
const key = o.board.toString();
let cache = db[key];
if (cache) {
if (alpha >= cache.upper) {
o.undo();
return cache.upper;
}
if (beta <= cache.lower) {
o.undo();
return cache.lower;
}
alpha = Math.max(alpha, cache.lower);
beta = Math.min(beta, cache.upper);
} else {
cache = {
upper: Infinity,
lower: -Infinity
};
}
const isBotTurn = o.turn === botColor;
// 勝った
if (o.turn === null) {
const winner = o.winner;
// 勝つことによる基本スコア
const base = 10000;
let score;
if (game.settings.isLlotheo) {
// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100);
} else {
// 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する
score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100);
}
// 巻き戻し
o.undo();
// 接待なら自分が負けた方が高スコア
return isSettai
? winner !== botColor ? score : -score
: winner === botColor ? score : -score;
}
if (depth === maxDepth) {
// 静的に評価
const score = staticEval();
// 巻き戻し
o.undo();
return score;
} else {
const cans = o.canPutSomewhere(o.turn);
let value = isBotTurn ? -Infinity : Infinity;
let a = alpha;
let b = beta;
// 次のターンのプレイヤーにとって最も良い手を取得
for (const p of cans) {
if (isBotTurn) {
const score = dive(p, a, beta, depth + 1);
value = Math.max(value, score);
a = Math.max(a, value);
if (value >= beta) break;
} else {
const score = dive(p, alpha, b, depth + 1);
value = Math.min(value, score);
b = Math.min(b, value);
if (value <= alpha) break;
}
}
// 巻き戻し
o.undo();
if (value <= alpha) {
cache.upper = value;
} else if (value >= beta) {
cache.lower = value;
} else {
cache.upper = value;
cache.lower = value;
}
db[key] = cache;
return value;
}
};
/**
* αβ()()
*/
const dive2 = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
// 試し打ち
o.put(o.turn, pos);
const isBotTurn = o.turn === botColor;
// 勝った
if (o.turn === null) {
const winner = o.winner;
// 勝つことによる基本スコア
const base = 10000;
let score;
if (game.settings.isLlotheo) {
// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100);
} else {
// 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する
score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100);
}
// 巻き戻し
o.undo();
// 接待なら自分が負けた方が高スコア
return isSettai
? winner !== botColor ? score : -score
: winner === botColor ? score : -score;
}
if (depth === maxDepth) {
// 静的に評価
const score = staticEval();
// 巻き戻し
o.undo();
return score;
} else {
const cans = o.canPutSomewhere(o.turn);
// 次のターンのプレイヤーにとって最も良い手を取得
for (const p of cans) {
if (isBotTurn) {
alpha = Math.max(alpha, dive2(p, alpha, beta, depth + 1));
} else {
beta = Math.min(beta, dive2(p, alpha, beta, depth + 1));
}
if (alpha >= beta) break;
}
// 巻き戻し
o.undo();
return isBotTurn ? alpha : beta;
}
};
const cans = o.canPutSomewhere(botColor);
const scores = cans.map(p => dive(p));
const pos = cans[scores.indexOf(Math.max(...scores))];
console.log('Thinked:', pos);
console.timeEnd('think');
process.send({
type: 'put',
pos
});
}

View file

@ -0,0 +1,233 @@
/**
* -AI-
* Botのフロントエンド()
*
*
*
*/
import * as childProcess from 'child_process';
const WebSocket = require('ws');
import * as ReconnectingWebSocket from 'reconnecting-websocket';
import * as request from 'request-promise-native';
import conf from '../../../conf';
// 設定 ////////////////////////////////////////////////////////
/**
* BotアカウントのAPIキー
*/
const i = conf.othello_ai.i;
/**
* BotアカウントのユーザーID
*/
const id = conf.othello_ai.id;
////////////////////////////////////////////////////////////////
/**
*
*/
const homeStream = new ReconnectingWebSocket(`${conf.ws_url}/?i=${i}`, undefined, {
constructor: WebSocket
});
homeStream.on('open', () => {
console.log('home stream opened');
});
homeStream.on('close', () => {
console.log('home stream closed');
});
homeStream.on('message', message => {
const msg = JSON.parse(message.toString());
// タイムライン上でなんか言われたまたは返信されたとき
if (msg.type == 'mention' || msg.type == 'reply') {
const post = msg.body;
if (post.userId == id) return;
// リアクションする
request.post(`${conf.api_url}/posts/reactions/create`, {
json: { i,
postId: post.id,
reaction: 'love'
}
});
if (post.text) {
if (post.text.indexOf('オセロ') > -1) {
request.post(`${conf.api_url}/posts/create`, {
json: { i,
replyId: post.id,
text: '良いですよ~'
}
});
invite(post.userId);
}
}
}
// メッセージでなんか言われたとき
if (msg.type == 'messaging_message') {
const message = msg.body;
if (message.text) {
if (message.text.indexOf('オセロ') > -1) {
request.post(`${conf.api_url}/messaging/messages/create`, {
json: { i,
userId: message.userId,
text: '良いですよ~'
}
});
invite(message.userId);
}
}
}
});
// ユーザーを対局に誘う
function invite(userId) {
request.post(`${conf.api_url}/othello/match`, {
json: { i,
userId: userId
}
});
}
/**
*
*/
const othelloStream = new ReconnectingWebSocket(`${conf.ws_url}/othello?i=${i}`, undefined, {
constructor: WebSocket
});
othelloStream.on('open', () => {
console.log('othello stream opened');
});
othelloStream.on('close', () => {
console.log('othello stream closed');
});
othelloStream.on('message', message => {
const msg = JSON.parse(message.toString());
// 招待されたとき
if (msg.type == 'invited') {
onInviteMe(msg.body.parent);
}
// マッチしたとき
if (msg.type == 'matched') {
gameStart(msg.body);
}
});
/**
*
* @param game
*/
function gameStart(game) {
// ゲームストリームに接続
const gw = new ReconnectingWebSocket(`${conf.ws_url}/othello-game?i=${i}&game=${game.id}`, undefined, {
constructor: WebSocket
});
gw.on('open', () => {
console.log('othello game stream opened');
// フォーム
const form = [{
id: 'strength',
type: 'radio',
label: '強さ',
value: 2,
items: [{
label: '接待',
value: 0
}, {
label: '弱',
value: 1
}, {
label: '中',
value: 2
}, {
label: '強',
value: 3
}, {
label: '最強',
value: 5
}]
}];
//#region バックエンドプロセス開始
const ai = childProcess.fork(__dirname + '/back.js');
// バックエンドプロセスに情報を渡す
ai.send({
type: '_init_',
game,
form
});
ai.on('message', msg => {
if (msg.type == 'put') {
gw.send(JSON.stringify({
type: 'set',
pos: msg.pos
}));
} else if (msg.type == 'close') {
gw.close();
}
});
// ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える
gw.on('message', message => {
const msg = JSON.parse(message.toString());
ai.send(msg);
});
//#endregion
// フォーム初期化
setTimeout(() => {
gw.send(JSON.stringify({
type: 'init-form',
body: form
}));
}, 1000);
// どんな設定内容の対局でも受け入れる
setTimeout(() => {
gw.send(JSON.stringify({
type: 'accept'
}));
}, 2000);
});
gw.on('close', () => {
console.log('othello game stream closed');
});
}
/**
*
* @param inviter
*/
async function onInviteMe(inviter) {
console.log(`Someone invited me: @${inviter.username}`);
// 承認
const game = await request.post(`${conf.api_url}/othello/match`, {
json: {
i,
userId: inviter.id
}
});
gameStart(game);
}

View file

@ -0,0 +1 @@
require('./front');

340
src/misc/othello/core.ts Normal file
View file

@ -0,0 +1,340 @@
/**
* true ...
* false ...
*/
export type Color = boolean;
const BLACK = true;
const WHITE = false;
export type MapPixel = 'null' | 'empty';
export type Options = {
isLlotheo: boolean;
canPutEverywhere: boolean;
loopedBoard: boolean;
};
export type Undo = {
/**
*
*/
color: Color,
/**
*
*/
pos: number;
/**
*
*/
effects: number[];
/**
*
*/
turn: Color;
};
/**
*
*/
export default class Othello {
public map: MapPixel[];
public mapWidth: number;
public mapHeight: number;
public board: Color[];
public turn: Color = BLACK;
public opts: Options;
public prevPos = -1;
public prevColor: Color = null;
private logs: Undo[] = [];
/**
*
*/
constructor(map: string[], opts: Options) {
//#region binds
this.put = this.put.bind(this);
//#endregion
//#region Options
this.opts = opts;
if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
//#endregion
//#region Parse map data
this.mapWidth = map[0].length;
this.mapHeight = map.length;
const mapData = map.join('');
this.board = mapData.split('').map(d => {
if (d == '-') return null;
if (d == 'b') return BLACK;
if (d == 'w') return WHITE;
return undefined;
});
this.map = mapData.split('').map(d => {
if (d == '-' || d == 'b' || d == 'w') return 'empty';
return 'null';
});
//#endregion
// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
if (this.canPutSomewhere(BLACK).length == 0) {
if (this.canPutSomewhere(WHITE).length == 0) {
this.turn = null;
} else {
this.turn = WHITE;
}
}
}
/**
*
*/
public get blackCount() {
return this.board.filter(x => x === BLACK).length;
}
/**
*
*/
public get whiteCount() {
return this.board.filter(x => x === WHITE).length;
}
/**
*
*/
public get blackP() {
if (this.blackCount == 0 && this.whiteCount == 0) return 0;
return this.blackCount / (this.blackCount + this.whiteCount);
}
/**
*
*/
public get whiteP() {
if (this.blackCount == 0 && this.whiteCount == 0) return 0;
return this.whiteCount / (this.blackCount + this.whiteCount);
}
public transformPosToXy(pos: number): number[] {
const x = pos % this.mapWidth;
const y = Math.floor(pos / this.mapWidth);
return [x, y];
}
public transformXyToPos(x: number, y: number): number {
return x + (y * this.mapWidth);
}
/**
*
* @param color
* @param pos
*/
public put(color: Color, pos: number) {
this.prevPos = pos;
this.prevColor = color;
this.board[pos] = color;
// 反転させられる石を取得
const effects = this.effects(color, pos);
// 反転させる
for (const pos of effects) {
this.board[pos] = color;
}
const turn = this.turn;
this.logs.push({
color,
pos,
effects,
turn
});
this.calcTurn();
}
private calcTurn() {
// ターン計算
if (this.canPutSomewhere(!this.prevColor).length > 0) {
this.turn = !this.prevColor;
} else if (this.canPutSomewhere(this.prevColor).length > 0) {
this.turn = this.prevColor;
} else {
this.turn = null;
}
}
public undo() {
const undo = this.logs.pop();
this.prevColor = undo.color;
this.prevPos = undo.pos;
this.board[undo.pos] = null;
for (const pos of undo.effects) {
const color = this.board[pos];
this.board[pos] = !color;
}
this.turn = undo.turn;
}
/**
*
* @param pos
*/
public mapDataGet(pos: number): MapPixel {
const [x, y] = this.transformPosToXy(pos);
if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) return 'null';
return this.map[pos];
}
/**
*
*/
public canPutSomewhere(color: Color): number[] {
const result = [];
this.board.forEach((x, i) => {
if (this.canPut(color, i)) result.push(i);
});
return result;
}
/**
*
* @param color
* @param pos
*/
public canPut(color: Color, pos: number): boolean {
// 既に石が置いてある場所には打てない
if (this.board[pos] !== null) return false;
if (this.opts.canPutEverywhere) {
// 挟んでなくても置けるモード
return this.mapDataGet(pos) == 'empty';
} else {
// 相手の石を1つでも反転させられるか
return this.effects(color, pos).length !== 0;
}
}
/**
*
* @param color
* @param pos
*/
public effects(color: Color, pos: number): number[] {
const enemyColor = !color;
// ひっくり返せる石(の位置)リスト
let stones = [];
const initPos = pos;
// 走査
const iterate = (fn: (i: number) => number[]) => {
let i = 1;
const found = [];
while (true) {
let [x, y] = fn(i);
// 座標が指し示す位置がボード外に出たとき
if (this.opts.loopedBoard) {
if (x < 0 ) x = this.mapWidth - ((-x) % this.mapWidth);
if (y < 0 ) y = this.mapHeight - ((-y) % this.mapHeight);
if (x >= this.mapWidth ) x = x % this.mapWidth;
if (y >= this.mapHeight) y = y % this.mapHeight;
// for debug
//if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) {
// console.log(x, y);
//}
// 一周して自分に帰ってきたら
if (this.transformXyToPos(x, y) == initPos) {
// ↓のコメントアウトを外すと、「現時点で自分の石が隣接していないが、
// そこに置いたとするとループして最終的に挟んだことになる」というケースを有効化します。(Test4のマップで違いが分かります)
// このケースを有効にした方が良いのか無効にした方が良いのか判断がつかなかったためとりあえず無効としておきます
// (あと無効な方がゲームとしておもしろそうだった)
stones = stones.concat(found);
break;
}
} else {
if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) break;
}
const pos = this.transformXyToPos(x, y);
//#region 「配置不能」マスに当たった場合走査終了
const pixel = this.mapDataGet(pos);
if (pixel == 'null') break;
//#endregion
// 石取得
const stone = this.board[pos];
// 石が置かれていないマスなら走査終了
if (stone === null) break;
// 相手の石なら「ひっくり返せるかもリスト」に入れておく
if (stone === enemyColor) found.push(pos);
// 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了
if (stone === color) {
stones = stones.concat(found);
break;
}
i++;
}
};
const [x, y] = this.transformPosToXy(pos);
iterate(i => [x , y - i]); // 上
iterate(i => [x + i, y - i]); // 右上
iterate(i => [x + i, y ]); // 右
iterate(i => [x + i, y + i]); // 右下
iterate(i => [x , y + i]); // 下
iterate(i => [x - i, y + i]); // 左下
iterate(i => [x - i, y ]); // 左
iterate(i => [x - i, y - i]); // 左上
return stones;
}
/**
*
*/
public get isEnded(): boolean {
return this.turn === null;
}
/**
* (null = )
*/
public get winner(): Color {
if (!this.isEnded) return undefined;
if (this.blackCount == this.whiteCount) return null;
if (this.opts.isLlotheo) {
return this.blackCount > this.whiteCount ? WHITE : BLACK;
} else {
return this.blackCount > this.whiteCount ? BLACK : WHITE;
}
}
}

911
src/misc/othello/maps.ts Normal file
View file

@ -0,0 +1,911 @@
/**
*
*
* :
* () ...
* - ...
* b ...
* w ...
*/
export type Map = {
name?: string;
category?: string;
author?: string;
data: string[];
};
export const fourfour: Map = {
name: '4x4',
category: '4x4',
data: [
'----',
'-wb-',
'-bw-',
'----'
]
};
export const sixsix: Map = {
name: '6x6',
category: '6x6',
data: [
'------',
'------',
'--wb--',
'--bw--',
'------',
'------'
]
};
export const roundedSixsix: Map = {
name: '6x6 rounded',
category: '6x6',
author: 'syuilo',
data: [
' ---- ',
'------',
'--wb--',
'--bw--',
'------',
' ---- '
]
};
export const roundedSixsix2: Map = {
name: '6x6 rounded 2',
category: '6x6',
author: 'syuilo',
data: [
' -- ',
' ---- ',
'--wb--',
'--bw--',
' ---- ',
' -- '
]
};
export const eighteight: Map = {
name: '8x8',
category: '8x8',
data: [
'--------',
'--------',
'--------',
'---wb---',
'---bw---',
'--------',
'--------',
'--------'
]
};
export const eighteightH1: Map = {
name: '8x8 handicap 1',
category: '8x8',
data: [
'b-------',
'--------',
'--------',
'---wb---',
'---bw---',
'--------',
'--------',
'--------'
]
};
export const eighteightH2: Map = {
name: '8x8 handicap 2',
category: '8x8',
data: [
'b-------',
'--------',
'--------',
'---wb---',
'---bw---',
'--------',
'--------',
'-------b'
]
};
export const eighteightH3: Map = {
name: '8x8 handicap 3',
category: '8x8',
data: [
'b------b',
'--------',
'--------',
'---wb---',
'---bw---',
'--------',
'--------',
'-------b'
]
};
export const eighteightH4: Map = {
name: '8x8 handicap 4',
category: '8x8',
data: [
'b------b',
'--------',
'--------',
'---wb---',
'---bw---',
'--------',
'--------',
'b------b'
]
};
export const eighteightH12: Map = {
name: '8x8 handicap 12',
category: '8x8',
data: [
'bb----bb',
'b------b',
'--------',
'---wb---',
'---bw---',
'--------',
'b------b',
'bb----bb'
]
};
export const eighteightH16: Map = {
name: '8x8 handicap 16',
category: '8x8',
data: [
'bbb---bb',
'b------b',
'-------b',
'---wb---',
'---bw---',
'b-------',
'b------b',
'bb---bbb'
]
};
export const eighteightH20: Map = {
name: '8x8 handicap 20',
category: '8x8',
data: [
'bbb--bbb',
'b------b',
'b------b',
'---wb---',
'---bw---',
'b------b',
'b------b',
'bbb---bb'
]
};
export const eighteightH28: Map = {
name: '8x8 handicap 28',
category: '8x8',
data: [
'bbbbbbbb',
'b------b',
'b------b',
'b--wb--b',
'b--bw--b',
'b------b',
'b------b',
'bbbbbbbb'
]
};
export const roundedEighteight: Map = {
name: '8x8 rounded',
category: '8x8',
author: 'syuilo',
data: [
' ------ ',
'--------',
'--------',
'---wb---',
'---bw---',
'--------',
'--------',
' ------ '
]
};
export const roundedEighteight2: Map = {
name: '8x8 rounded 2',
category: '8x8',
author: 'syuilo',
data: [
' ---- ',
' ------ ',
'--------',
'---wb---',
'---bw---',
'--------',
' ------ ',
' ---- '
]
};
export const roundedEighteight3: Map = {
name: '8x8 rounded 3',
category: '8x8',
author: 'syuilo',
data: [
' -- ',
' ---- ',
' ------ ',
'---wb---',
'---bw---',
' ------ ',
' ---- ',
' -- '
]
};
export const eighteightWithNotch: Map = {
name: '8x8 with notch',
category: '8x8',
author: 'syuilo',
data: [
'--- ---',
'--------',
'--------',
' --wb-- ',
' --bw-- ',
'--------',
'--------',
'--- ---'
]
};
export const eighteightWithSomeHoles: Map = {
name: '8x8 with some holes',
category: '8x8',
author: 'syuilo',
data: [
'--- ----',
'----- --',
'-- -----',
'---wb---',
'---bw- -',
' -------',
'--- ----',
'--------'
]
};
export const circle: Map = {
name: 'Circle',
category: '8x8',
author: 'syuilo',
data: [
' -- ',
' ------ ',
' ------ ',
'---wb---',
'---bw---',
' ------ ',
' ------ ',
' -- '
]
};
export const smile: Map = {
name: 'Smile',
category: '8x8',
author: 'syuilo',
data: [
' ------ ',
'--------',
'-- -- --',
'---wb---',
'-- bw --',
'--- ---',
'--------',
' ------ '
]
};
export const window: Map = {
name: 'Window',
category: '8x8',
author: 'syuilo',
data: [
'--------',
'- -- -',
'- -- -',
'---wb---',
'---bw---',
'- -- -',
'- -- -',
'--------'
]
};
export const reserved: Map = {
name: 'Reserved',
category: '8x8',
author: 'Aya',
data: [
'w------b',
'--------',
'--------',
'---wb---',
'---bw---',
'--------',
'--------',
'b------w'
]
};
export const x: Map = {
name: 'X',
category: '8x8',
author: 'Aya',
data: [
'w------b',
'-w----b-',
'--w--b--',
'---wb---',
'---bw---',
'--b--w--',
'-b----w-',
'b------w'
]
};
export const parallel: Map = {
name: 'Parallel',
category: '8x8',
author: 'Aya',
data: [
'--------',
'--------',
'--------',
'---bb---',
'---ww---',
'--------',
'--------',
'--------'
]
};
export const lackOfBlack: Map = {
name: 'Lack of Black',
category: '8x8',
data: [
'--------',
'--------',
'--------',
'---w----',
'---bw---',
'--------',
'--------',
'--------'
]
};
export const squareParty: Map = {
name: 'Square Party',
category: '8x8',
author: 'syuilo',
data: [
'--------',
'-wwwbbb-',
'-w-wb-b-',
'-wwwbbb-',
'-bbbwww-',
'-b-bw-w-',
'-bbbwww-',
'--------'
]
};
export const minesweeper: Map = {
name: 'Minesweeper',
category: '8x8',
author: 'syuilo',
data: [
'b-b--w-w',
'-w-wb-b-',
'w-b--w-b',
'-b-wb-w-',
'-w-bw-b-',
'b-w--b-w',
'-b-bw-w-',
'w-w--b-b'
]
};
export const tenthtenth: Map = {
name: '10x10',
category: '10x10',
data: [
'----------',
'----------',
'----------',
'----------',
'----wb----',
'----bw----',
'----------',
'----------',
'----------',
'----------'
]
};
export const hole: Map = {
name: 'The Hole',
category: '10x10',
author: 'syuilo',
data: [
'----------',
'----------',
'--wb--wb--',
'--bw--bw--',
'---- ----',
'---- ----',
'--wb--wb--',
'--bw--bw--',
'----------',
'----------'
]
};
export const grid: Map = {
name: 'Grid',
category: '10x10',
author: 'syuilo',
data: [
'----------',
'- - -- - -',
'----------',
'- - -- - -',
'----wb----',
'----bw----',
'- - -- - -',
'----------',
'- - -- - -',
'----------'
]
};
export const cross: Map = {
name: 'Cross',
category: '10x10',
author: 'Aya',
data: [
' ---- ',
' ---- ',
' ---- ',
'----------',
'----wb----',
'----bw----',
'----------',
' ---- ',
' ---- ',
' ---- '
]
};
export const charX: Map = {
name: 'Char X',
category: '10x10',
author: 'syuilo',
data: [
'--- ---',
'---- ----',
'----------',
' -------- ',
' --wb-- ',
' --bw-- ',
' -------- ',
'----------',
'---- ----',
'--- ---'
]
};
export const charY: Map = {
name: 'Char Y',
category: '10x10',
author: 'syuilo',
data: [
'--- ---',
'---- ----',
'----------',
' -------- ',
' --wb-- ',
' --bw-- ',
' ------ ',
' ------ ',
' ------ ',
' ------ '
]
};
export const walls: Map = {
name: 'Walls',
category: '10x10',
author: 'Aya',
data: [
' bbbbbbbb ',
'w--------w',
'w--------w',
'w--------w',
'w---wb---w',
'w---bw---w',
'w--------w',
'w--------w',
'w--------w',
' bbbbbbbb '
]
};
export const cpu: Map = {
name: 'CPU',
category: '10x10',
author: 'syuilo',
data: [
' b b b b ',
'w--------w',
' -------- ',
'w--------w',
' ---wb--- ',
' ---bw--- ',
'w--------w',
' -------- ',
'w--------w',
' b b b b '
]
};
export const checker: Map = {
name: 'Checker',
category: '10x10',
author: 'Aya',
data: [
'----------',
'----------',
'----------',
'---wbwb---',
'---bwbw---',
'---wbwb---',
'---bwbw---',
'----------',
'----------',
'----------'
]
};
export const japaneseCurry: Map = {
name: 'Japanese curry',
category: '10x10',
author: 'syuilo',
data: [
'w-b-b-b-b-',
'-w-b-b-b-b',
'w-w-b-b-b-',
'-w-w-b-b-b',
'w-w-wwb-b-',
'-w-wbb-b-b',
'w-w-w-b-b-',
'-w-w-w-b-b',
'w-w-w-w-b-',
'-w-w-w-w-b'
]
};
export const mosaic: Map = {
name: 'Mosaic',
category: '10x10',
author: 'syuilo',
data: [
'- - - - - ',
' - - - - -',
'- - - - - ',
' - w w - -',
'- - b b - ',
' - w w - -',
'- - b b - ',
' - - - - -',
'- - - - - ',
' - - - - -',
]
};
export const arena: Map = {
name: 'Arena',
category: '10x10',
author: 'syuilo',
data: [
'- - -- - -',
' - - - - ',
'- ------ -',
' -------- ',
'- --wb-- -',
'- --bw-- -',
' -------- ',
'- ------ -',
' - - - - ',
'- - -- - -'
]
};
export const reactor: Map = {
name: 'Reactor',
category: '10x10',
author: 'syuilo',
data: [
'-w------b-',
'b- - - -w',
'- --wb-- -',
'---b w---',
'- b wb w -',
'- w bw b -',
'---w b---',
'- --bw-- -',
'w- - - -b',
'-b------w-'
]
};
export const sixeight: Map = {
name: '6x8',
category: 'Special',
data: [
'------',
'------',
'------',
'--wb--',
'--bw--',
'------',
'------',
'------'
]
};
export const spark: Map = {
name: 'Spark',
category: 'Special',
author: 'syuilo',
data: [
' - - ',
'----------',
' -------- ',
' -------- ',
' ---wb--- ',
' ---bw--- ',
' -------- ',
' -------- ',
'----------',
' - - '
]
};
export const islands: Map = {
name: 'Islands',
category: 'Special',
author: 'syuilo',
data: [
'-------- ',
'---wb--- ',
'---bw--- ',
'-------- ',
' - - ',
' - - ',
' --------',
' --------',
' --------',
' --------'
]
};
export const galaxy: Map = {
name: 'Galaxy',
category: 'Special',
author: 'syuilo',
data: [
' ------ ',
' --www--- ',
' ------w--- ',
'---bbb--w---',
'--b---b-w-b-',
'-b--wwb-w-b-',
'-b-w-bww--b-',
'-b-w-b---b--',
'---w--bbb---',
' ---w------ ',
' ---www-- ',
' ------ '
]
};
export const triangle: Map = {
name: 'Triangle',
category: 'Special',
author: 'syuilo',
data: [
' -- ',
' -- ',
' ---- ',
' ---- ',
' --wb-- ',
' --bw-- ',
' -------- ',
' -------- ',
'----------',
'----------'
]
};
export const iphonex: Map = {
name: 'iPhone X',
category: 'Special',
author: 'syuilo',
data: [
' -- -- ',
'--------',
'--------',
'--------',
'--------',
'---wb---',
'---bw---',
'--------',
'--------',
'--------',
'--------',
' ------ '
]
};
export const dealWithIt: Map = {
name: 'Deal with it!',
category: 'Special',
author: 'syuilo',
data: [
'------------',
'--w-b-------',
' --b-w------',
' --w-b---- ',
' ------- '
]
};
export const experiment: Map = {
name: 'Let\'s experiment',
category: 'Special',
author: 'syuilo',
data: [
' ------------ ',
'------wb------',
'------bw------',
'--------------',
' - - ',
'------ ------',
'bbbbbb wwwwww',
'bbbbbb wwwwww',
'bbbbbb wwwwww',
'bbbbbb wwwwww',
'wwwwww bbbbbb'
]
};
export const bigBoard: Map = {
name: 'Big board',
category: 'Special',
data: [
'----------------',
'----------------',
'----------------',
'----------------',
'----------------',
'----------------',
'----------------',
'-------wb-------',
'-------bw-------',
'----------------',
'----------------',
'----------------',
'----------------',
'----------------',
'----------------',
'----------------'
]
};
export const twoBoard: Map = {
name: 'Two board',
category: 'Special',
author: 'Aya',
data: [
'-------- --------',
'-------- --------',
'-------- --------',
'---wb--- ---wb---',
'---bw--- ---bw---',
'-------- --------',
'-------- --------',
'-------- --------'
]
};
export const test1: Map = {
name: 'Test1',
category: 'Test',
data: [
'--------',
'---wb---',
'---bw---',
'--------'
]
};
export const test2: Map = {
name: 'Test2',
category: 'Test',
data: [
'------',
'------',
'-b--w-',
'-w--b-',
'-w--b-'
]
};
export const test3: Map = {
name: 'Test3',
category: 'Test',
data: [
'-w-',
'--w',
'w--',
'-w-',
'--w',
'w--',
'-w-',
'--w',
'w--',
'-w-',
'---',
'b--',
]
};
export const test4: Map = {
name: 'Test4',
category: 'Test',
data: [
'-w--b-',
'-w--b-',
'------',
'-w--b-',
'-w--b-'
]
};
// https://misskey.xyz/othello/5aaabf7fe126e10b5216ea09 64
export const test5: Map = {
name: 'Test5',
category: 'Test',
data: [
'--wwwwww--',
'--wwwbwwww',
'-bwwbwbwww',
'-bwwwbwbww',
'-bwwbwbwbw',
'-bwbwbwb-w',
'bwbwwbbb-w',
'w-wbbbbb--',
'--w-b-w---',
'----------'
]
};

View file

@ -0,0 +1,3 @@
export default user => {
return user.host === null ? user.username : `${user.username}@${user.host}`;
};

View file

@ -0,0 +1,18 @@
import { IUser, isLocalUser } from '../../models/user';
import getAcct from './get-acct';
/**
*
* @param user
*/
export default function(user: IUser): string {
let string = `${user.name} (@${getAcct(user)})\n` +
`${user.postsCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
if (isLocalUser(user)) {
const account = user.account;
string += `場所: ${account.profile.location}、誕生日: ${account.profile.birthday}\n`;
}
return string + `${user.description}`;
}

View file

@ -0,0 +1,4 @@
export default acct => {
const splitted = acct.split('@', 2);
return { username: splitted[0], host: splitted[1] || null };
};