Improve othello ai
This commit is contained in:
		
							parent
							
								
									88a06dfff7
								
							
						
					
					
						commit
						02d22943c4
					
				
					 5 changed files with 526 additions and 42 deletions
				
			
		| 
						 | 
					@ -69,6 +69,7 @@
 | 
				
			||||||
		"@types/ratelimiter": "2.1.28",
 | 
							"@types/ratelimiter": "2.1.28",
 | 
				
			||||||
		"@types/redis": "2.8.5",
 | 
							"@types/redis": "2.8.5",
 | 
				
			||||||
		"@types/request": "2.47.0",
 | 
							"@types/request": "2.47.0",
 | 
				
			||||||
 | 
							"@types/request-promise-native": "^1.0.14",
 | 
				
			||||||
		"@types/rimraf": "2.0.2",
 | 
							"@types/rimraf": "2.0.2",
 | 
				
			||||||
		"@types/seedrandom": "2.4.27",
 | 
							"@types/seedrandom": "2.4.27",
 | 
				
			||||||
		"@types/serve-favicon": "2.2.30",
 | 
							"@types/serve-favicon": "2.2.30",
 | 
				
			||||||
| 
						 | 
					@ -78,6 +79,7 @@
 | 
				
			||||||
		"@types/webpack": "3.8.8",
 | 
							"@types/webpack": "3.8.8",
 | 
				
			||||||
		"@types/webpack-stream": "3.2.9",
 | 
							"@types/webpack-stream": "3.2.9",
 | 
				
			||||||
		"@types/websocket": "0.0.37",
 | 
							"@types/websocket": "0.0.37",
 | 
				
			||||||
 | 
							"@types/ws": "^4.0.1",
 | 
				
			||||||
		"accesses": "2.5.0",
 | 
							"accesses": "2.5.0",
 | 
				
			||||||
		"animejs": "2.2.0",
 | 
							"animejs": "2.2.0",
 | 
				
			||||||
		"autosize": "4.0.0",
 | 
							"autosize": "4.0.0",
 | 
				
			||||||
| 
						 | 
					@ -158,6 +160,7 @@
 | 
				
			||||||
		"reconnecting-websocket": "3.2.2",
 | 
							"reconnecting-websocket": "3.2.2",
 | 
				
			||||||
		"redis": "2.8.0",
 | 
							"redis": "2.8.0",
 | 
				
			||||||
		"request": "2.83.0",
 | 
							"request": "2.83.0",
 | 
				
			||||||
 | 
							"request-promise-native": "^1.0.5",
 | 
				
			||||||
		"rimraf": "2.6.2",
 | 
							"rimraf": "2.6.2",
 | 
				
			||||||
		"rndstr": "1.0.0",
 | 
							"rndstr": "1.0.0",
 | 
				
			||||||
		"s-age": "1.1.2",
 | 
							"s-age": "1.1.2",
 | 
				
			||||||
| 
						 | 
					@ -198,6 +201,7 @@
 | 
				
			||||||
		"webpack-cli": "^2.0.8",
 | 
							"webpack-cli": "^2.0.8",
 | 
				
			||||||
		"webpack-replace-loader": "1.3.0",
 | 
							"webpack-replace-loader": "1.3.0",
 | 
				
			||||||
		"websocket": "1.0.25",
 | 
							"websocket": "1.0.25",
 | 
				
			||||||
 | 
							"ws": "^5.0.0",
 | 
				
			||||||
		"xev": "2.0.0"
 | 
							"xev": "2.0.0"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,42 +0,0 @@
 | 
				
			||||||
import Othello, { Color } from './core';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function ai(color: Color, othello: Othello) {
 | 
					 | 
				
			||||||
	//const opponentColor = color == 'black' ? 'white' : 'black';
 | 
					 | 
				
			||||||
/* wip
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	function think() {
 | 
					 | 
				
			||||||
		// 打てる場所を取得
 | 
					 | 
				
			||||||
		const ps = othello.canPutSomewhere(color);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (ps.length > 0) { // 打てる場所がある場合
 | 
					 | 
				
			||||||
			// 角を取得
 | 
					 | 
				
			||||||
			const corners = ps.filter(p =>
 | 
					 | 
				
			||||||
				// 左上
 | 
					 | 
				
			||||||
				(p[0] == 0 && p[1] == 0) ||
 | 
					 | 
				
			||||||
				// 右上
 | 
					 | 
				
			||||||
				(p[0] == (BOARD_SIZE - 1) && p[1] == 0) ||
 | 
					 | 
				
			||||||
				// 右下
 | 
					 | 
				
			||||||
				(p[0] == (BOARD_SIZE - 1) && p[1] == (BOARD_SIZE - 1)) ||
 | 
					 | 
				
			||||||
				// 左下
 | 
					 | 
				
			||||||
				(p[0] == 0 && p[1] == (BOARD_SIZE - 1))
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (corners.length > 0) { // どこかしらの角に打てる場合
 | 
					 | 
				
			||||||
				// 打てる角からランダムに選択して打つ
 | 
					 | 
				
			||||||
				const p = corners[Math.floor(Math.random() * corners.length)];
 | 
					 | 
				
			||||||
				othello.set(color, p[0], p[1]);
 | 
					 | 
				
			||||||
			} else { // 打てる角がない場合
 | 
					 | 
				
			||||||
				// 打てる場所からランダムに選択して打つ
 | 
					 | 
				
			||||||
				const p = ps[Math.floor(Math.random() * ps.length)];
 | 
					 | 
				
			||||||
				othello.set(color, p[0], p[1]);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 相手の打つ場所がない場合続けてAIのターン
 | 
					 | 
				
			||||||
			if (othello.getPattern(opponentColor).length === 0) {
 | 
					 | 
				
			||||||
				think();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	think();*/
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										287
									
								
								src/common/othello/ai/back.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								src/common/othello/ai/back.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,287 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 接待モードのときは、全力(5手先読みくらい)で負けるようにする
 | 
				
			||||||
 | 
						const maxDepth = isSettai ? 5 : 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) => {
 | 
				
			||||||
 | 
									// 係数
 | 
				
			||||||
 | 
									const coefficient = 30;
 | 
				
			||||||
 | 
									s = s * coefficient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									const stone = o.board[i];
 | 
				
			||||||
 | 
									if (stone === botColor) {
 | 
				
			||||||
 | 
										// TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する
 | 
				
			||||||
 | 
										score += s;
 | 
				
			||||||
 | 
									} else if (stone !== null) {
 | 
				
			||||||
 | 
										score -= s;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 巻き戻し
 | 
				
			||||||
 | 
								o.undo(undo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// ロセオならスコアを反転
 | 
				
			||||||
 | 
								if (game.settings.is_llotheo) score = -score;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 接待ならスコアを反転
 | 
				
			||||||
 | 
								if (isSettai) score = -score;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								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(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
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										231
									
								
								src/common/othello/ai/front.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								src/common/othello/ai/front.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,231 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * -AI-
 | 
				
			||||||
 | 
					 * Botのフロントエンド(ストリームとの対話を担当)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから
 | 
				
			||||||
 | 
					 * 切断されてしまうので、別々のプロセスで行うようにします
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as childProcess from 'child_process';
 | 
				
			||||||
 | 
					import * as WebSocket from 'ws';
 | 
				
			||||||
 | 
					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 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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -74,6 +74,10 @@ type Source = {
 | 
				
			||||||
		hook_secret: string;
 | 
							hook_secret: string;
 | 
				
			||||||
		username: string;
 | 
							username: string;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
						othello_ai?: {
 | 
				
			||||||
 | 
							id: string;
 | 
				
			||||||
 | 
							i: string;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
	line_bot?: {
 | 
						line_bot?: {
 | 
				
			||||||
		channel_secret: string;
 | 
							channel_secret: string;
 | 
				
			||||||
		channel_access_token: string;
 | 
							channel_access_token: string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue