From b18e4fea9851ce0692b3f1a627c1d220d5f16b9c Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Oct 2017 03:24:10 +0900 Subject: [PATCH] :v: --- src/api/bot/core.ts | 51 ++++++ src/api/bot/interfaces/line.ts | 108 +++++++------ src/common/othello.ts | 275 +++++++++++++++++++++++++++++++++ 3 files changed, 391 insertions(+), 43 deletions(-) create mode 100644 src/common/othello.ts diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts index bc5818d97..6042862d3 100644 --- a/src/api/bot/core.ts +++ b/src/api/bot/core.ts @@ -6,6 +6,8 @@ import User, { IUser, init as initUser } from '../models/user'; import getPostSummary from '../../common/get-post-summary'; import getUserSummary from '../../common/get-user-summary'; +import Othello, { ai as othelloAi } from '../../common/othello'; + /** * Botの頭脳 */ @@ -106,6 +108,11 @@ export default class BotCore extends EventEmitter { case 'タイムライン': return await this.tlCommand(); + case 'othello': + case 'オセロ': + this.setContext(new OthelloContext(this)); + return await this.context.greet(); + default: return '?'; } @@ -124,6 +131,18 @@ export default class BotCore extends EventEmitter { this.emit('updated'); } + public async refreshUser() { + this.user = await User.findOne({ + _id: this.user._id + }, { + fields: { + data: false + } + }); + + this.emit('updated'); + } + public async tlCommand(): Promise { if (this.user == null) return 'まずサインインしてください。'; @@ -166,6 +185,7 @@ abstract class Context extends EventEmitter { } public static import(bot: BotCore, data: any) { + if (data.type == 'othello') return OthelloContext.import(bot, data.content); if (data.type == 'post') return PostContext.import(bot, data.content); if (data.type == 'signin') return SigninContext.import(bot, data.content); return null; @@ -251,3 +271,34 @@ class PostContext extends Context { return context; } } + +class OthelloContext extends Context { + private othello: Othello = null; + + public async greet(): Promise { + this.othello = new Othello(); + return this.othello.toPatternString('black'); + } + + public async q(query: string): Promise { + this.othello.setByNumber('black', parseInt(query, 10)); + othelloAi('white', this.othello); + return this.othello.toPatternString('black'); + } + + public export() { + return { + type: 'othello', + content: { + board: this.othello.board + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new OthelloContext(bot); + context.othello = new Othello(); + context.othello.board = data.board; + return context; + } +} diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 2bf62c1f6..0caa71ed2 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -50,38 +50,44 @@ class LineBot extends BotCore { public async react(ev: any): Promise { this.replyToken = ev.replyToken; - // メッセージ - if (ev.type == 'message') { - // テキスト - if (ev.message.type == 'text') { - const res = await this.q(ev.message.text); + switch (ev.type) { + // メッセージ + case 'message': + switch (ev.message.type) { + // テキスト + case 'text': + const res = await this.q(ev.message.text); + if (res == null) return; + // 返信 + this.reply([{ + type: 'text', + text: res + }]); + break; - if (res == null) return; + // スタンプ + case 'sticker': + // スタンプで返信 + this.reply([{ + type: 'sticker', + packageId: '4', + stickerId: stickers[Math.floor(Math.random() * stickers.length)] + }]); + break; + } + break; - // 返信 - this.reply([{ - type: 'text', - text: res - }]); - // スタンプ - } else if (ev.message.type == 'sticker') { - // スタンプで返信 - this.reply([{ - type: 'sticker', - packageId: '4', - stickerId: stickers[Math.floor(Math.random() * stickers.length)] - }]); - } - // postback - } else if (ev.type == 'postback') { - const data = ev.postback.data; - const cmd = data.split('|')[0]; - const arg = data.split('|')[1]; - switch (cmd) { - case 'showtl': - this.showUserTimelinePostback(arg); - break; - } + // postback + case 'postback': + const data = ev.postback.data; + const cmd = data.split('|')[0]; + const arg = data.split('|')[1]; + switch (cmd) { + case 'showtl': + this.showUserTimelinePostback(arg); + break; + } + break; } } @@ -96,6 +102,28 @@ class LineBot extends BotCore { username: q.substr(1) }, this.user); + const actions = []; + + actions.push({ + type: 'postback', + label: 'タイムラインを見る', + data: `showtl|${user.id}` + }); + + if (user.twitter) { + actions.push({ + type: 'uri', + label: 'Twitterアカウントを見る', + uri: `https://twitter.com/${user.twitter.screen_name}` + }); + } + + actions.push({ + type: 'uri', + label: 'Webで見る', + uri: `${config.url}/${user.username}` + }); + this.reply([{ type: 'template', altText: await super.showUserCommand(q), @@ -104,15 +132,7 @@ class LineBot extends BotCore { thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`, title: `${user.name} (@${user.username})`, text: user.description || '(no description)', - actions: [{ - type: 'postback', - label: 'タイムラインを見る', - data: `showtl|${user.id}` - }, { - type: 'uri', - label: 'Webで見る', - uri: `${config.url}/${user.username}` - }] + actions: actions } }]); } @@ -123,7 +143,7 @@ class LineBot extends BotCore { limit: 5 }, this.user); - const text = tl + const text = `${tl[0].user.name}さんのタイムラインはこちらです:\n\n` + tl .map(post => getPostSummary(post)) .join('\n-----\n'); @@ -144,10 +164,10 @@ module.exports = async (app: express.Application) => { const sourceId = ev.source.userId; const sessionId = `line-bot-sessions:${sourceId}`; - const _session = await redis.get(sessionId); + const session = await redis.get(sessionId); let bot: LineBot; - if (_session == null) { + if (session == null) { const user = await User.findOne({ line: { user_id: sourceId @@ -178,13 +198,15 @@ module.exports = async (app: express.Application) => { redis.set(sessionId, JSON.stringify(bot.export())); } else { - bot = LineBot.import(JSON.parse(_session)); + bot = LineBot.import(JSON.parse(session)); } bot.on('updated', () => { redis.set(sessionId, JSON.stringify(bot.export())); }); + if (session != null) bot.refreshUser(); + bot.react(ev); }); diff --git a/src/common/othello.ts b/src/common/othello.ts new file mode 100644 index 000000000..006040197 --- /dev/null +++ b/src/common/othello.ts @@ -0,0 +1,275 @@ +import * as EventEmitter from 'events'; + +export default class Othello extends EventEmitter { + public board: Array>; + + /** + * ゲームを初期化します + */ + constructor() { + super(); + + this.board = [ + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null], + [null, null, null, 'black', 'white', null, null, null], + [null, null, null, 'white', 'black', null, null, null], + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null] + ]; + } + + public setByNumber(color, n) { + const ps = this.getPattern(color); + this.set(color, ps[n][0], ps[n][1]); + } + + /** + * 石を配置します + */ + public set(color, x, y) { + this.board[y][x] = color; + + const reverses = this.getReverse(color, x, y); + + reverses.forEach(r => { + switch (r[0]) { + case 0: // 上 + for (let c = 0, _y = y - 1; c < r[1]; c++, _y--) { + this.board[x][_y] = color; + } + break; + + case 1: // 右上 + for (let c = 0, i = 1; c < r[1]; c++, i++) { + this.board[x + i][y - i] = color; + } + break; + + case 2: // 右 + for (let c = 0, _x = x + 1; c < r[1]; c++, _x++) { + this.board[_x][y] = color; + } + break; + + case 3: // 右下 + for (let c = 0, i = 1; c < r[1]; c++, i++) { + this.board[x + i][y + i] = color; + } + break; + + case 4: // 下 + for (let c = 0, _y = y + 1; c < r[1]; c++, _y++) { + this.board[x][_y] = color; + } + break; + + case 5: // 左下 + for (let c = 0, i = 1; c < r[1]; c++, i++) { + this.board[x - i][y + i] = color; + } + break; + + case 6: // 左 + for (let c = 0, _x = x - 1; c < r[1]; c++, _x--) { + this.board[_x][y] = color; + } + break; + + case 7: // 左上 + for (let c = 0, i = 1; c < r[1]; c++, i++) { + this.board[x - i][y - i] = color; + } + break; + } + }); + + this.emit('set:' + color, x, y); + } + + /** + * 打つことができる場所を取得します + */ + public getPattern(myColor): number[][] { + const result = []; + this.board.forEach((stones, y) => stones.forEach((stone, x) => { + if (stone != null) return; + if (this.canReverse(myColor, x, y)) result.push([x, y]); + })); + return result; + } + + /** + * 指定の位置に石を打つことができるかどうか(相手の石を1つでも反転させられるか)を取得します + */ + public canReverse(myColor, targetx, targety): boolean { + return this.getReverse(myColor, targetx, targety) !== null; + } + + private getReverse(myColor, targetx, targety): number[] { + const opponentColor = myColor == 'black' ? 'white' : 'black'; + + const createIterater = () => { + let opponentStoneFound = false; + let breaked = false; + return (x, y): any => { + if (breaked) { + return; + } else if (this.board[x][y] == myColor && opponentStoneFound) { + return true; + } else if (this.board[x][y] == myColor && !opponentStoneFound) { + breaked = true; + } else if (this.board[x][y] == opponentColor) { + opponentStoneFound = true; + } else { + breaked = true; + } + }; + }; + + const res = []; + + let iterate; + + // 上 + iterate = createIterater(); + for (let c = 0, y = targety - 1; y >= 0; c++, y--) { + if (iterate(targetx, y)) { + res.push([0, c]); + break; + } + } + + // 右上 + iterate = createIterater(); + for (let c = 0, i = 1; i < Math.min(8 - targetx, targety); c++, i++) { + if (iterate(targetx + i, targety - i)) { + res.push([1, c]); + break; + } + } + + // 右 + iterate = createIterater(); + for (let c = 0, x = targetx + 1; x < 8; c++, x++) { + if (iterate(x, targety)) { + res.push([2, c]); + break; + } + } + + // 右下 + iterate = createIterater(); + for (let c = 0, i = 1; i < Math.min(8 - targetx, 8 - targety); c++, i++) { + if (iterate(targetx + i, targety + i)) { + res.push([3, c]); + break; + } + } + + // 下 + iterate = createIterater(); + for (let c = 0, y = targety + 1; y < 8; c++, y++) { + if (iterate(targetx, y)) { + res.push([4, c]); + break; + } + } + + // 左下 + iterate = createIterater(); + for (let c = 0, i = 1; i < Math.min(targetx, 8 - targety); c++, i++) { + if (iterate(targetx - i, targety + i)) { + res.push([5, c]); + break; + } + } + + // 左 + iterate = createIterater(); + for (let c = 0, x = targetx - 1; x >= 0; c++, x--) { + if (iterate(x, targety)) { + res.push([6, c]); + break; + } + } + + // 左上 + iterate = createIterater(); + for (let c = 0, i = 1; i < Math.min(targetx, targety); c++, i++) { + if (iterate(targetx - i, targety - i)) { + res.push([7, c]); + break; + } + } + + return res.length === 0 ? null : res; + } + + public toString(): string { + return this.board.map(row => row.map(state => state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : '🔹').join('')).join('\n'); + } + + public toPatternString(color): string { + const num = ['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟']; + + const pattern = this.getPattern(color); + + return this.board.map((row, y) => row.map((state, x) => { + const i = pattern.findIndex(p => p[0] == x && p[1] == y); + return state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : i != -1 ? num[i] : '🔹'; + }).join('')).join('\n'); + } +} +/* +export class Ai { + private othello: Othello; + private color: string; + private opponentColor: string; + + constructor(color: string, othello: Othello) { + this.othello = othello; + this.color = color; + this.opponentColor = this.color == 'black' ? 'white' : 'black'; + + this.othello.on('set:' + this.opponentColor, () => { + this.turn(); + }); + + if (this.color == 'black') { + this.turn(); + } + } + + public turn() { + const ps = this.othello.getPattern(this.color); + if (ps.length > 0) { + const p = ps[Math.floor(Math.random() * ps.length)]; + this.othello.set(this.color, p[0], p[1]); + + // 相手の打つ場所がない場合続けてAIのターン + if (this.othello.getPattern(this.opponentColor).length === 0) { + this.turn(); + } + } + } +} +*/ +export function ai(color: string, othello: Othello) { + const opponentColor = color == 'black' ? 'white' : 'black'; + + function think() { + const ps = othello.getPattern(color); + if (ps.length > 0) { + const p = ps[Math.floor(Math.random() * ps.length)]; + othello.set(color, p[0], p[1]); + + // 相手の打つ場所がない場合続けてAIのターン + if (othello.getPattern(opponentColor).length === 0) { + think(); + } + } + } +}