1
0
mirror of https://github.com/misskey-dev/misskey synced 2024-12-22 10:38:28 +09:00
misskey/src/othello/ai/back.ts

378 lines
9.2 KiB
TypeScript
Raw Normal View History

2018-03-14 04:20:15 +09:00
/**
* -AI-
* Botのバックエンド()
*
*
*
*/
2018-03-15 20:19:26 +09:00
import * as request from 'request-promise-native';
2018-03-14 04:20:15 +09:00
import Othello, { Color } from '../core';
2018-04-02 13:15:53 +09:00
import conf from '../../config';
2018-04-06 01:36:34 +09:00
import getUserName from '../../renderers/get-user-name';
2018-03-14 04:20:15 +09:00
let game;
let form;
/**
2018-03-15 20:19:26 +09:00
* BotアカウントのユーザーID
2018-03-14 04:20:15 +09:00
*/
2018-03-15 20:19:26 +09:00
const id = conf.othello_ai.id;
2018-03-14 04:20:15 +09:00
2018-03-15 20:19:26 +09:00
/**
* BotアカウントのAPIキー
*/
const i = conf.othello_ai.i;
2018-04-08 02:30:37 +09:00
let note;
2018-03-15 20:19:26 +09:00
process.on('message', async msg => {
2018-03-14 04:20:15 +09:00
// 親プロセスからデータをもらう
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;
2018-03-16 22:38:28 +09:00
const url = `${conf.url}/othello/${game.id}`;
2018-03-29 14:48:47 +09:00
const user = game.user1Id == id ? game.user2 : game.user1;
2018-03-14 04:20:15 +09:00
const isSettai = form[0].value === 0;
const text = isSettai
2018-04-06 01:36:34 +09:00
? `?[${getUserName(user)}](${conf.url}/@${user.username})さんの接待を始めました!`
: `対局を?[${getUserName(user)}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`;
2018-03-16 22:38:28 +09:00
2018-04-08 02:30:37 +09:00
const res = await request.post(`${conf.api_url}/notes/create`, {
2018-03-15 20:19:26 +09:00
json: { i,
text: `${text}\n→[観戦する](${url})`
}
2018-03-14 04:20:15 +09:00
});
2018-03-16 22:38:28 +09:00
2018-04-08 02:30:37 +09:00
note = res.createdNote;
2018-03-14 04:20:15 +09:00
//#endregion
}
// ゲームが終了したとき
if (msg.type == 'ended') {
// ストリームから切断
process.send({
type: 'close'
});
//#region TLに投稿する
2018-03-29 14:48:47 +09:00
const user = game.user1Id == id ? game.user2 : game.user1;
2018-03-14 04:20:15 +09:00
const isSettai = form[0].value === 0;
const text = isSettai
2018-03-29 14:48:47 +09:00
? msg.body.winnerId === null
2018-04-06 01:36:34 +09:00
? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で引き分けました...`
2018-03-29 14:48:47 +09:00
: msg.body.winnerId == id
2018-04-06 01:36:34 +09:00
? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...`
: `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で負けてあげました♪`
2018-03-29 14:48:47 +09:00
: msg.body.winnerId === null
2018-04-06 01:36:34 +09:00
? `?[${getUserName(user)}](${conf.url}/@${user.username})さんと引き分けました~`
2018-03-29 14:48:47 +09:00
: msg.body.winnerId == id
2018-04-06 01:36:34 +09:00
? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに勝ちました♪`
: `?[${getUserName(user)}](${conf.url}/@${user.username})さんに負けました...`;
2018-03-16 22:38:28 +09:00
2018-04-08 02:30:37 +09:00
await request.post(`${conf.api_url}/notes/create`, {
2018-03-15 20:19:26 +09:00
json: { i,
2018-04-08 02:30:37 +09:00
renoteId: note.id,
2018-03-15 20:19:26 +09:00
text: text
}
2018-03-14 04:20:15 +09:00
});
//#endregion
process.exit();
2018-03-14 04:20:15 +09:00
}
// 打たれたとき
if (msg.type == 'set') {
onSet(msg.body);
}
});
let o: Othello;
let botColor: Color;
// 各マスの強さ
2018-03-16 23:38:53 +09:00
let cellWeights;
2018-03-14 04:20:15 +09:00
/**
*
* @param g
*/
function onGameStarted(g) {
game = g;
// オセロエンジン初期化
o = new Othello(game.settings.map, {
2018-03-29 14:48:47 +09:00
isLlotheo: game.settings.isLlotheo,
canPutEverywhere: game.settings.canPutEverywhere,
loopedBoard: game.settings.loopedBoard
2018-03-14 04:20:15 +09:00
});
// 各マスの価値を計算しておく
2018-03-16 23:38:53 +09:00
cellWeights = o.map.map((pix, i) => {
2018-03-14 04:20:15 +09:00
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);
2018-03-16 22:23:53 +09:00
return count >= 4 ? 1 : 0;
2018-03-14 04:20:15 +09:00
});
2018-03-29 14:48:47 +09:00
botColor = game.user1Id == id && game.black == 1 || game.user2Id == id && game.black == 2;
2018-03-14 04:20:15 +09:00
if (botColor) {
think();
}
}
function onSet(x) {
2018-03-15 14:09:38 +09:00
o.put(x.color, x.pos);
2018-03-14 04:20:15 +09:00
if (x.next === botColor) {
think();
}
}
2018-03-16 23:38:53 +09:00
const db = {};
2018-03-14 04:20:15 +09:00
function think() {
console.log('Thinking...');
2018-03-16 23:38:53 +09:00
console.time('think');
2018-03-14 04:20:15 +09:00
const isSettai = form[0].value === 0;
// 接待モードのときは、全力(5手先読みくらい)で負けるようにする
const maxDepth = isSettai ? 5 : form[0].value;
2018-03-16 23:38:53 +09:00
/**
* 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;
}
});
// ロセオならスコアを反転
2018-03-29 14:48:47 +09:00
if (game.settings.isLlotheo) score = -score;
2018-03-16 23:38:53 +09:00
// 接待ならスコアを反転
if (isSettai) score = -score;
return score;
}
2018-03-14 04:20:15 +09:00
/**
* αβ
*/
2018-03-16 23:38:53 +09:00
const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
2018-03-14 04:20:15 +09:00
// 試し打ち
2018-03-15 14:09:38 +09:00
o.put(o.turn, pos);
2018-03-14 04:20:15 +09:00
const key = o.board.toString();
let cache = db[key];
if (cache) {
if (alpha >= cache.upper) {
2018-03-15 14:09:38 +09:00
o.undo();
2018-03-14 04:20:15 +09:00
return cache.upper;
}
if (beta <= cache.lower) {
2018-03-15 14:09:38 +09:00
o.undo();
2018-03-14 04:20:15 +09:00
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;
2018-03-29 14:48:47 +09:00
if (game.settings.isLlotheo) {
2018-03-14 04:20:15 +09:00
// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100);
} else {
// 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する
score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100);
}
// 巻き戻し
2018-03-15 14:09:38 +09:00
o.undo();
2018-03-14 04:20:15 +09:00
// 接待なら自分が負けた方が高スコア
return isSettai
? winner !== botColor ? score : -score
: winner === botColor ? score : -score;
}
if (depth === maxDepth) {
2018-03-16 23:38:53 +09:00
// 静的に評価
const score = staticEval();
2018-03-14 04:20:15 +09:00
// 巻き戻し
2018-03-15 14:09:38 +09:00
o.undo();
2018-03-14 04:20:15 +09:00
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) {
2018-03-16 23:38:53 +09:00
const score = dive(p, a, beta, depth + 1);
2018-03-14 04:20:15 +09:00
value = Math.max(value, score);
a = Math.max(a, value);
if (value >= beta) break;
} else {
2018-03-16 23:38:53 +09:00
const score = dive(p, alpha, b, depth + 1);
2018-03-14 04:20:15 +09:00
value = Math.min(value, score);
b = Math.min(b, value);
if (value <= alpha) break;
}
}
// 巻き戻し
2018-03-15 14:09:38 +09:00
o.undo();
2018-03-14 04:20:15 +09:00
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;
}
};
2018-03-16 23:38:53 +09:00
/**
* αβ()()
*/
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;
2018-03-29 14:48:47 +09:00
if (game.settings.isLlotheo) {
2018-03-16 23:38:53 +09:00
// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
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));
}
2018-03-17 00:22:22 +09:00
if (alpha >= beta) break;
2018-03-16 23:38:53 +09:00
}
// 巻き戻し
o.undo();
return isBotTurn ? alpha : beta;
}
};
2018-03-14 04:20:15 +09:00
const cans = o.canPutSomewhere(botColor);
2018-03-16 23:38:53 +09:00
const scores = cans.map(p => dive(p));
2018-03-14 04:20:15 +09:00
const pos = cans[scores.indexOf(Math.max(...scores))];
console.log('Thinked:', pos);
2018-03-16 23:38:53 +09:00
console.timeEnd('think');
2018-03-14 04:20:15 +09:00
process.send({
type: 'put',
pos
});
}