This commit is contained in:
syuilo 2018-08-05 20:55:27 +09:00
parent 296db18f54
commit e7152ddeec
2 changed files with 302 additions and 233 deletions

View file

@ -9,12 +9,9 @@
import * as request from 'request-promise-native'; import * as request from 'request-promise-native';
import Reversi, { Color } from 'misskey-reversi'; import Reversi, { Color } from 'misskey-reversi';
let game;
let form;
const config = require('../config.json'); const config = require('../config.json');
let note; const db = {};
function getUserName(user) { function getUserName(user) {
return user.name || user.username; return user.name || user.username;
@ -27,159 +24,189 @@ const titles = [
'先生', 'せんせい', 'センセイ', 'センセイ' '先生', 'せんせい', 'センセイ', 'センセイ'
]; ];
process.on('message', async msg => { class Session {
// 親プロセスからデータをもらう private game: any;
if (msg.type == '_init_') { private form: any;
game = msg.game; private o: Reversi;
form = msg.form; private botColor: Color;
/**
*
*/
private cellWeights;
/**
* 稿
*/
private startedNote: any = null;
private get user(): any {
return this.game.user1Id == config.id ? this.game.user2 : this.game.user1;
} }
// フォームが更新されたとき private get userName(): string {
if (msg.type == 'update-form') { return `?[${getUserName(this.user)}](${config.host}/@${this.user.username})${titles.some(x => this.user.username.endsWith(x)) ? '' : 'さん'}`;
form.find(i => i.id == msg.body.id).value = msg.body.value;
} }
// ゲームが始まったとき private get strength(): number {
if (msg.type == 'started') { return this.form.find(i => i.id == 'strength').value;
onGameStarted(msg.body); }
//#region TLに投稿する private get isSettai(): boolean {
if (form.find(i => i.id == 'publish').value) { return this.strength === 0;
const game = msg.body; }
const url = `${config.host}/reversi/${game.id}`;
const user = game.user1Id == config.id ? game.user2 : game.user1;
const strength = form.find(i => i.id == 'strength').value;
const isSettai = strength === 0;
const text = isSettai
? `?[${getUserName(user)}](${config.host}/@${user.username})${titles.some(x => user.username.endsWith(x)) ? '' : 'さん'}の接待を始めました!`
: `対局を?[${getUserName(user)}](${config.host}/@${user.username})${titles.some(x => user.username.endsWith(x)) ? '' : 'さん'}と始めました! (強さ${strength})`;
const res = await request.post(`${config.host}/api/notes/create`, { private get allowPost(): boolean {
json: { return this.form.find(i => i.id == 'publish').value;
i: config.i, }
text: `${text}\n→[観戦する](${url})`
}
});
note = res.createdNote; private get url(): string {
return `${config.host}/reversi/${this.game.id}`;
}
constructor() {
process.on('message', this.onMessage);
}
private onMessage = async (msg: any) => {
switch (msg.type) {
case '_init_': this.onInit(msg); break;
case 'update-form': this.onUpdateForn(msg); break;
case 'started': this.onStarted(msg); break;
case 'ended': this.onEnded(msg); break;
case 'set': this.onSet(msg); break;
} }
//#endregion
} }
// ゲームが終了したとき // 親プロセスからデータをもらう
if (msg.type == 'ended') { private onInit = (msg: any) => {
this.game = msg.game;
this.form = msg.form;
}
/**
*
*/
private onUpdateForn = (msg: any) => {
this.form.find(i => i.id == msg.body.id).value = msg.body.value;
}
/**
*
*/
private onStarted = (msg: any) => {
this.game = msg.body;
// TLに投稿する
this.postGameStarted().then(note => {
this.startedNote = note;
});
// リバーシエンジン初期化
this.o = new Reversi(this.game.settings.map, {
isLlotheo: this.game.settings.isLlotheo,
canPutEverywhere: this.game.settings.canPutEverywhere,
loopedBoard: this.game.settings.loopedBoard
});
// 各マスの価値を計算しておく
this.cellWeights = this.o.map.map((pix, i) => {
if (pix == 'null') return 0;
const [x, y] = this.o.transformPosToXy(i);
let count = 0;
const get = (x, y) => {
if (x < 0 || y < 0 || x >= this.o.mapWidth || y >= this.o.mapHeight) return 'null';
return this.o.mapDataGet(this.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 >= 4 ? 1 : 0;
});
this.botColor = this.game.user1Id == config.id && this.game.black == 1 || this.game.user2Id == config.id && this.game.black == 2;
if (this.botColor) {
this.think();
}
}
/**
*
*/
private onEnded = (msg: any) => {
// ストリームから切断 // ストリームから切断
process.send({ process.send({
type: 'close' type: 'close'
}); });
//#region TLに投稿する let text: string;
if (form.find(i => i.id == 'publish').value) {
const user = game.user1Id == config.id ? game.user2 : game.user1; if (msg.body.game.surrendered) {
const strength = form.find(i => i.id == 'strength').value; if (this.isSettai) {
const isSettai = strength === 0; text = `(${this.userName}を接待していたら投了されちゃいました... ごめんなさい)`;
const text = `?[${getUserName(user)}](${config.host}/@${user.username})${titles.some(x => user.username.endsWith(x)) ? '' : 'さん'}${msg.body.game.surrendered ? 'が投了しちゃいました' : msg.body.winnerId ? msg.body.winnerId == config.id ? (isSettai ? 'に接待で勝ってしまいました...' : 'に勝ちました♪') : (isSettai ? 'に接待で負けてあげました♪' : 'に負けました...') : (isSettai ? 'に接待で引き分けました...' : 'と引き分けました~')}`; } else {
await request.post(`${config.host}/api/notes/create`, { text = `${this.userName}が投了しちゃいました`;
json: { }
i: config.i, } else if (msg.body.winnerId) {
renoteId: note ? note.id : null, if (msg.body.winnerId == config.id) {
text: text if (this.isSettai) {
text = `${this.userName}に接待で勝ってしまいました...`;
} else {
text = `${this.userName}に勝ちました♪`;
} }
}); } else {
if (this.isSettai) {
text = `(${this.userName}に接待で負けてあげました...♪)`;
} else {
text = `${this.userName}に負けました...`;
}
}
} else {
if (this.isSettai) {
text = `(${this.userName}に接待で引き分けました...)`;
} else {
text = `${this.userName}と引き分けました~`;
}
} }
//#endregion
this.post(text, this.startedNote ? this.startedNote.id : null);
process.exit(); process.exit();
} }
// 打たれたとき /**
if (msg.type == 'set') { *
onSet(msg.body); */
private onSet = (msg: any) => {
this.o.put(msg.body.color, msg.body.pos);
if (msg.body.next === this.botColor) {
this.think();
}
} }
});
let o: Reversi;
let botColor: Color;
// 各マスの強さ
let cellWeights;
/**
*
* @param g
*/
function onGameStarted(g) {
game = g;
// リバーシエンジン初期化
o = new Reversi(game.settings.map, {
isLlotheo: game.settings.isLlotheo,
canPutEverywhere: game.settings.canPutEverywhere,
loopedBoard: game.settings.loopedBoard
});
// 各マスの価値を計算しておく
cellWeights = 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 >= 4 ? 1 : 0;
});
botColor = game.user1Id == config.id && game.black == 1 || game.user2Id == config.id && game.black == 2;
if (botColor) {
think();
}
}
function onSet(x) {
o.put(x.color, x.pos);
if (x.next === botColor) {
think();
}
}
const db = {};
function think() {
console.log('Thinking...');
console.time('think');
const strength = form.find(i => i.id == 'strength').value;
const isSettai = strength === 0;
// 接待モードのときは、全力(5手先読みくらい)で負けるようにする
const maxDepth = isSettai ? 5 : strength;
/** /**
* Botにとってある局面がどれだけ有利か取得する * Botにとってある局面がどれだけ有利か取得する
*/ */
function staticEval() { private staticEval = () => {
let score = o.canPutSomewhere(botColor).length; let score = this.o.canPutSomewhere(this.botColor).length;
cellWeights.forEach((weight, i) => { this.cellWeights.forEach((weight, i) => {
// 係数 // 係数
const coefficient = 30; const coefficient = 30;
weight = weight * coefficient; weight = weight * coefficient;
const stone = o.board[i]; const stone = this.o.board[i];
if (stone === botColor) { if (stone === this.botColor) {
// TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する // TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する
score += weight; score += weight;
} else if (stone !== null) { } else if (stone !== null) {
@ -188,126 +215,167 @@ function think() {
}); });
// ロセオならスコアを反転 // ロセオならスコアを反転
if (game.settings.isLlotheo) score = -score; if (this.game.settings.isLlotheo) score = -score;
// 接待ならスコアを反転 // 接待ならスコアを反転
if (isSettai) score = -score; if (this.isSettai) score = -score;
return score; return score;
} }
/** private think = () => {
* αβ console.log('Thinking...');
*/ console.time('think');
const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
// 試し打ち
o.put(o.turn, pos);
const key = o.board.toString(); // 接待モードのときは、全力(5手先読みくらい)で負けるようにする
let cache = db[key]; const maxDepth = this.isSettai ? 5 : this.strength;
if (cache) {
if (alpha >= cache.upper) {
o.undo();
return cache.upper;
}
if (beta <= cache.lower) {
o.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; /**
* αβ
*/
const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
// 試し打ち
this.o.put(this.o.turn, pos);
// 勝った const key = this.o.board.toString();
if (o.turn === null) { let cache = db[key];
const winner = o.winner; if (cache) {
if (alpha >= cache.upper) {
// 勝つことによる基本スコア this.o.undo();
const base = 10000; return cache.upper;
let score;
if (game.settings.isLlotheo) {
// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
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);
let value = isBotTurn ? -Infinity : Infinity;
let a = alpha;
let b = beta;
// 次のターンのプレイヤーにとって最も良い手を取得
for (const p of cans) {
if (isBotTurn) {
const score = dive(p, a, beta, depth + 1);
value = Math.max(value, score);
a = Math.max(a, value);
if (value >= beta) break;
} else {
const score = dive(p, alpha, b, depth + 1);
value = Math.min(value, score);
b = Math.min(b, value);
if (value <= alpha) break;
} }
} if (beta <= cache.lower) {
this.o.undo();
// 巻き戻し return cache.lower;
o.undo(); }
alpha = Math.max(alpha, cache.lower);
if (value <= alpha) { beta = Math.min(beta, cache.upper);
cache.upper = value;
} else if (value >= beta) {
cache.lower = value;
} else { } else {
cache.upper = value; cache = {
cache.lower = value; upper: Infinity,
lower: -Infinity
};
} }
db[key] = cache; const isBotTurn = this.o.turn === this.botColor;
return value; // 勝った
if (this.o.turn === null) {
const winner = this.o.winner;
// 勝つことによる基本スコア
const base = 10000;
let score;
if (this.game.settings.isLlotheo) {
// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
score = this.o.winner ? base - (this.o.blackCount * 100) : base - (this.o.whiteCount * 100);
} else {
// 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する
score = this.o.winner ? base + (this.o.blackCount * 100) : base + (this.o.whiteCount * 100);
}
// 巻き戻し
this.o.undo();
// 接待なら自分が負けた方が高スコア
return this.isSettai
? winner !== this.botColor ? score : -score
: winner === this.botColor ? score : -score;
}
if (depth === maxDepth) {
// 静的に評価
const score = this.staticEval();
// 巻き戻し
this.o.undo();
return score;
} else {
const cans = this.o.canPutSomewhere(this.o.turn);
let value = isBotTurn ? -Infinity : Infinity;
let a = alpha;
let b = beta;
// 次のターンのプレイヤーにとって最も良い手を取得
for (const p of cans) {
if (isBotTurn) {
const score = dive(p, a, beta, depth + 1);
value = Math.max(value, score);
a = Math.max(a, value);
if (value >= beta) break;
} else {
const score = dive(p, alpha, b, depth + 1);
value = Math.min(value, score);
b = Math.min(b, value);
if (value <= alpha) break;
}
}
// 巻き戻し
this.o.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 = this.o.canPutSomewhere(this.botColor);
const scores = cans.map(p => dive(p));
const pos = cans[scores.indexOf(Math.max(...scores))];
console.log('Thinked:', pos);
console.timeEnd('think');
process.send({
type: 'put',
pos
});
}
/**
* Misskeyに投稿します
*/
private postGameStarted = async () => {
const text = this.isSettai
? `${this.userName}の接待を始めました!`
: `対局を${this.userName}と始めました! (強さ${this.strength})`;
return await this.post(`${text}\n→[観戦する](${this.url})`);
}
/**
* Misskeyに投稿します
* @param text 稿
*/
private post = async (text: string, renote?: any) => {
if (this.allowPost) {
const res = await request.post(`${config.host}/api/notes/create`, {
json: {
i: config.i,
text: text,
renoteId: renote ? renote.id : null
}
});
return res.createdNote;
} else {
return null;
} }
}; }
const cans = o.canPutSomewhere(botColor);
const scores = cans.map(p => dive(p));
const pos = cans[scores.indexOf(Math.max(...scores))];
console.log('Thinked:', pos);
console.timeEnd('think');
process.send({
type: 'put',
pos
});
} }
new Session();

View file

@ -3,6 +3,7 @@
"noEmitOnError": true, "noEmitOnError": true,
"noImplicitAny": false, "noImplicitAny": false,
"noImplicitReturns": true, "noImplicitReturns": true,
"noImplicitThis": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"sourceMap": false, "sourceMap": false,