Improve othello ai

This commit is contained in:
syuilo 2018-03-14 04:20:15 +09:00
parent 88a06dfff7
commit 02d22943c4
5 changed files with 526 additions and 42 deletions

View file

@ -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"
} }
} }

View file

@ -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();*/
}

View 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
});
}

View 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);
}

View file

@ -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;