Last active
August 3, 2019 10:58
-
-
Save syuilo/226df4c027a197f0282012101b16d619 to your computer and use it in GitHub Desktop.
AI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * -AI- | |
| * Botのバックエンド(思考を担当) | |
| * | |
| * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから | |
| * 切断されてしまうので、別々のプロセスで行うようにします | |
| */ | |
| import Othello, { Color } from '../core'; | |
| let game; | |
| let form; | |
| /** | |
| * このBotのユーザーID | |
| */ | |
| let id; | |
| process.on('message', msg => { | |
| console.log(msg); | |
| // 親プロセスからデータをもらう | |
| if (msg.type == '_init_') { | |
| game = msg.game; | |
| form = msg.form; | |
| id = msg.id; | |
| } | |
| // フォームが更新されたとき | |
| 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 = `https://misskey.xyz/othello/${game.id}`; | |
| const user = game.user1_id == id ? game.user2 : game.user1; | |
| const isSettai = form[0].value === 0; | |
| const text = isSettai | |
| ? `?[${user.name}](https://misskey.xyz/${user.username})さんの接待を始めました!` | |
| : `対局を?[${user.name}](https://misskey.xyz/${user.username})さんと始めました! (強さ${form[0].value})`; | |
| process.send({ | |
| type: 'tl', | |
| text: `${text}\n→[観戦する](${url})` | |
| }); | |
| //#endregion | |
| } | |
| // ゲームが終了したとき | |
| if (msg.type == 'ended') { | |
| // ストリームから切断 | |
| process.send({ | |
| type: 'close' | |
| }); | |
| //#region TLに投稿する | |
| const url = `https://misskey.xyz/othello/${msg.body.game.id}`; | |
| const user = game.user1_id == id ? game.user2 : game.user1; | |
| const isSettai = form[0].value === 0; | |
| const text = isSettai | |
| ? msg.body.winner_id === null | |
| ? `?[${user.name}](https://misskey.xyz/${user.username})さんに接待で引き分けました...` | |
| : msg.body.winner_id == id | |
| ? `?[${user.name}](https://misskey.xyz/${user.username})さんに接待で勝ってしまいました...` | |
| : `?[${user.name}](https://misskey.xyz/${user.username})さんに接待で負けてあげました♪` | |
| : msg.body.winner_id === null | |
| ? `?[${user.name}](https://misskey.xyz/${user.username})さんと引き分けました~ (強さ${form[0].value})` | |
| : msg.body.winner_id == id | |
| ? `?[${user.name}](https://misskey.xyz/${user.username})さんに勝ちました♪ (強さ${form[0].value})` | |
| : `?[${user.name}](https://misskey.xyz/${user.username})さんに負けました... (強さ${form[0].value})`; | |
| process.send({ | |
| type: 'tl', | |
| text: `${text}\n→[結果を見る](${url})` | |
| }); | |
| //#endregion | |
| } | |
| // 打たれたとき | |
| if (msg.type == 'set') { | |
| onSet(msg.body); | |
| } | |
| }); | |
| let o: Othello; | |
| let botColor: Color; | |
| // 各マスの強さ | |
| let cellStrongs; | |
| /** | |
| * ゲーム開始時 | |
| * @param g ゲーム情報 | |
| */ | |
| function onGameStarted(g) { | |
| game = g; | |
| // オセロエンジン初期化 | |
| o = new Othello(game.settings.map, { | |
| isLlotheo: game.settings.is_llotheo, | |
| canPutEverywhere: game.settings.can_put_everywhere, | |
| loopedBoard: game.settings.looped_board | |
| }); | |
| // 各マスの価値を計算しておく | |
| cellStrongs = 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 >= 5 ? 1 : 0; | |
| }); | |
| botColor = game.user1_id == id && game.black == 1 || game.user2_id == id && game.black == 2; | |
| if (botColor) { | |
| think(); | |
| } | |
| } | |
| function onSet(x) { | |
| o.put(x.color, x.pos, true); | |
| if (x.next === botColor) { | |
| think(); | |
| } | |
| } | |
| function think() { | |
| console.log('Thinking...'); | |
| const isSettai = form[0].value === 0; | |
| // 接待モードのときは、全力(4手先読みくらい)で負けるようにする | |
| const maxDepth = isSettai ? 4 : form[0].value; | |
| const db = {}; | |
| /** | |
| * αβ法での探索 | |
| */ | |
| const dive = (o: Othello, pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { | |
| // 試し打ち | |
| const undo = o.put(o.turn, pos, true); | |
| const key = o.board.toString(); | |
| let cache = db[key]; | |
| if (cache) { | |
| if (alpha >= cache.upper) { | |
| o.undo(undo); | |
| return cache.upper; | |
| } | |
| if (beta <= cache.lower) { | |
| o.undo(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.is_llotheo) { | |
| // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する | |
| 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(undo); | |
| // 接待なら自分が負けた方が高スコア | |
| return isSettai | |
| ? winner !== botColor ? score : -score | |
| : winner === botColor ? score : -score; | |
| } | |
| if (depth === maxDepth) { | |
| let score = o.canPutSomewhere(botColor).length; | |
| cellStrongs.forEach((s, i) => { | |
| s = s * 30; | |
| const stone = o.board[i]; | |
| if (stone === botColor) { | |
| if (game.settings.is_llotheo) { | |
| score -= s; | |
| } else { | |
| score += s; | |
| } | |
| } else if (stone != null) { | |
| if (game.settings.is_llotheo) { | |
| score += s; | |
| } else { | |
| score -= s; | |
| } | |
| } | |
| }); | |
| // 巻き戻し | |
| o.undo(undo); | |
| // 接待ならスコアを反転 | |
| return isSettai ? -score : 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(o, p, a, beta, depth + 1); | |
| value = Math.max(value, score); | |
| a = Math.max(a, value); | |
| if (value >= beta) break; | |
| } else { | |
| const score = dive(o, p, alpha, b, depth + 1); | |
| value = Math.min(value, score); | |
| b = Math.min(b, value); | |
| if (value <= alpha) break; | |
| } | |
| } | |
| // 巻き戻し | |
| o.undo(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 cans = o.canPutSomewhere(botColor); | |
| const scores = cans.map(p => dive(o, p)); | |
| const pos = cans[scores.indexOf(Math.max(...scores))]; | |
| console.log('Thinked:', pos); | |
| process.send({ | |
| type: 'put', | |
| pos | |
| }); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * -AI- | |
| * Botのフロントエンド(ストリームとの対話を担当) | |
| * | |
| * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから | |
| * 切断されてしまうので、別々のプロセスで行うようにします | |
| */ | |
| import * as childProcess from 'child_process'; | |
| import * as WebSocket from 'ws'; | |
| import * as request from 'request-promise-native'; | |
| // 設定 //////////////////////////////////////////////////////// | |
| /** | |
| * BotアカウントのAPIキー | |
| */ | |
| const i = ''; | |
| /** | |
| * BotアカウントのユーザーID | |
| */ | |
| const id = ''; | |
| //////////////////////////////////////////////////////////////// | |
| /** | |
| * ホームストリーム | |
| */ | |
| const homeStream = new WebSocket(`wss://api.misskey.xyz/?i=${i}`); | |
| 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; | |
| // リアクションする | |
| request.post('https://api.misskey.xyz/posts/reactions/create', { | |
| json: { i, | |
| post_id: post.id, | |
| reaction: 'love' | |
| } | |
| }); | |
| if (post.text) { | |
| if (post.text.indexOf('オセロ') > -1) { | |
| request.post('https://api.misskey.xyz/posts/create', { | |
| json: { i, | |
| reply_id: post.id, | |
| text: '良いですよ~' | |
| } | |
| }); | |
| invite(post.user_id); | |
| } | |
| } | |
| } | |
| // メッセージでなんか言われたとき | |
| if (msg.type == 'messaging_message') { | |
| const message = msg.body; | |
| if (message.text) { | |
| if (message.text.indexOf('オセロ') > -1) { | |
| request.post('https://api.misskey.xyz/messaging/messages/create', { | |
| json: { i, | |
| user_id: message.user_id, | |
| text: '良いですよ~' | |
| } | |
| }); | |
| invite(message.user_id); | |
| } | |
| } | |
| } | |
| }); | |
| function invite(userId) { | |
| request.post('https://api.misskey.xyz/othello/match', { | |
| json: { i, | |
| user_id: userId | |
| } | |
| }); | |
| } | |
| /** | |
| * オセロストリーム | |
| */ | |
| const othelloStream = new WebSocket(`wss://api.misskey.xyz/othello?i=${i}`); | |
| 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 WebSocket(`wss://api.misskey.xyz/othello-game?i=${i}&game=${game.id}`); | |
| 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, | |
| id | |
| }); | |
| ai.on('message', msg => { | |
| if (msg.type == 'put') { | |
| gw.send(JSON.stringify({ | |
| type: 'set', | |
| pos: msg.pos | |
| })); | |
| } else if (msg.type == 'tl') { | |
| request.post('https://api.misskey.xyz/posts/create', { | |
| json: { i, | |
| text: msg.text | |
| } | |
| }); | |
| } 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(`Anybody invited me: @${inviter.username}`); | |
| // 承認 | |
| const game = await request.post('https://api.misskey.xyz/othello/match', { | |
| json: { | |
| i, | |
| user_id: inviter.id | |
| } | |
| }); | |
| gameStart(game); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment