mirror of
https://github.com/syuilo/ai.git
synced 2024-11-09 15:38:00 +00:00
✌️
This commit is contained in:
parent
296db18f54
commit
e7152ddeec
344
src/back.ts
344
src/back.ts
|
@ -9,12 +9,9 @@
|
|||
import * as request from 'request-promise-native';
|
||||
import Reversi, { Color } from 'misskey-reversi';
|
||||
|
||||
let game;
|
||||
let form;
|
||||
|
||||
const config = require('../config.json');
|
||||
|
||||
let note;
|
||||
const db = {};
|
||||
|
||||
function getUserName(user) {
|
||||
return user.name || user.username;
|
||||
|
@ -27,105 +24,100 @@ const titles = [
|
|||
'先生', 'せんせい', 'センセイ', 'センセイ'
|
||||
];
|
||||
|
||||
process.on('message', async msg => {
|
||||
// 親プロセスからデータをもらう
|
||||
if (msg.type == '_init_') {
|
||||
game = msg.game;
|
||||
form = msg.form;
|
||||
}
|
||||
class Session {
|
||||
private game: any;
|
||||
private form: any;
|
||||
private o: Reversi;
|
||||
private botColor: Color;
|
||||
|
||||
// フォームが更新されたとき
|
||||
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に投稿する
|
||||
if (form.find(i => i.id == 'publish').value) {
|
||||
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`, {
|
||||
json: {
|
||||
i: config.i,
|
||||
text: `${text}\n→[観戦する](${url})`
|
||||
}
|
||||
});
|
||||
|
||||
note = res.createdNote;
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
// ゲームが終了したとき
|
||||
if (msg.type == 'ended') {
|
||||
// ストリームから切断
|
||||
process.send({
|
||||
type: 'close'
|
||||
});
|
||||
|
||||
//#region TLに投稿する
|
||||
if (form.find(i => i.id == 'publish').value) {
|
||||
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 = `?[${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 ? 'に接待で引き分けました...' : 'と引き分けました~')}`;
|
||||
await request.post(`${config.host}/api/notes/create`, {
|
||||
json: {
|
||||
i: config.i,
|
||||
renoteId: note ? note.id : null,
|
||||
text: text
|
||||
}
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// 打たれたとき
|
||||
if (msg.type == 'set') {
|
||||
onSet(msg.body);
|
||||
}
|
||||
});
|
||||
|
||||
let o: Reversi;
|
||||
let botColor: Color;
|
||||
|
||||
// 各マスの強さ
|
||||
let cellWeights;
|
||||
|
||||
/**
|
||||
* ゲーム開始時
|
||||
* @param g ゲーム情報
|
||||
/**
|
||||
* 各マスの強さ
|
||||
*/
|
||||
function onGameStarted(g) {
|
||||
game = g;
|
||||
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 {
|
||||
return `?[${getUserName(this.user)}](${config.host}/@${this.user.username})${titles.some(x => this.user.username.endsWith(x)) ? '' : 'さん'}`;
|
||||
}
|
||||
|
||||
private get strength(): number {
|
||||
return this.form.find(i => i.id == 'strength').value;
|
||||
}
|
||||
|
||||
private get isSettai(): boolean {
|
||||
return this.strength === 0;
|
||||
}
|
||||
|
||||
private get allowPost(): boolean {
|
||||
return this.form.find(i => i.id == 'publish').value;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 親プロセスからデータをもらう
|
||||
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;
|
||||
});
|
||||
|
||||
// リバーシエンジン初期化
|
||||
o = new Reversi(game.settings.map, {
|
||||
isLlotheo: game.settings.isLlotheo,
|
||||
canPutEverywhere: game.settings.canPutEverywhere,
|
||||
loopedBoard: game.settings.loopedBoard
|
||||
this.o = new Reversi(this.game.settings.map, {
|
||||
isLlotheo: this.game.settings.isLlotheo,
|
||||
canPutEverywhere: this.game.settings.canPutEverywhere,
|
||||
loopedBoard: this.game.settings.loopedBoard
|
||||
});
|
||||
|
||||
// 各マスの価値を計算しておく
|
||||
cellWeights = o.map.map((pix, i) => {
|
||||
this.cellWeights = this.o.map.map((pix, i) => {
|
||||
if (pix == 'null') return 0;
|
||||
const [x, y] = o.transformPosToXy(i);
|
||||
const [x, y] = this.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 (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++;
|
||||
|
@ -140,46 +132,81 @@ function onGameStarted(g) {
|
|||
return count >= 4 ? 1 : 0;
|
||||
});
|
||||
|
||||
botColor = game.user1Id == config.id && game.black == 1 || game.user2Id == config.id && game.black == 2;
|
||||
this.botColor = this.game.user1Id == config.id && this.game.black == 1 || this.game.user2Id == config.id && this.game.black == 2;
|
||||
|
||||
if (botColor) {
|
||||
think();
|
||||
if (this.botColor) {
|
||||
this.think();
|
||||
}
|
||||
}
|
||||
|
||||
function onSet(x) {
|
||||
o.put(x.color, x.pos);
|
||||
|
||||
if (x.next === botColor) {
|
||||
think();
|
||||
}
|
||||
}
|
||||
|
||||
const db = {};
|
||||
/**
|
||||
* 対局が終わったとき
|
||||
*/
|
||||
private onEnded = (msg: any) => {
|
||||
// ストリームから切断
|
||||
process.send({
|
||||
type: 'close'
|
||||
});
|
||||
|
||||
function think() {
|
||||
console.log('Thinking...');
|
||||
console.time('think');
|
||||
let text: string;
|
||||
|
||||
const strength = form.find(i => i.id == 'strength').value;
|
||||
const isSettai = strength === 0;
|
||||
if (msg.body.game.surrendered) {
|
||||
if (this.isSettai) {
|
||||
text = `(${this.userName}を接待していたら投了されちゃいました... ごめんなさい)`;
|
||||
} else {
|
||||
text = `${this.userName}が投了しちゃいました`;
|
||||
}
|
||||
} else if (msg.body.winnerId) {
|
||||
if (msg.body.winnerId == config.id) {
|
||||
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}と引き分けました~`;
|
||||
}
|
||||
}
|
||||
|
||||
// 接待モードのときは、全力(5手先読みくらい)で負けるようにする
|
||||
const maxDepth = isSettai ? 5 : strength;
|
||||
this.post(text, this.startedNote ? this.startedNote.id : null);
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打たれたとき
|
||||
*/
|
||||
private onSet = (msg: any) => {
|
||||
this.o.put(msg.body.color, msg.body.pos);
|
||||
|
||||
if (msg.body.next === this.botColor) {
|
||||
this.think();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Botにとってある局面がどれだけ有利か取得する
|
||||
*/
|
||||
function staticEval() {
|
||||
let score = o.canPutSomewhere(botColor).length;
|
||||
private staticEval = () => {
|
||||
let score = this.o.canPutSomewhere(this.botColor).length;
|
||||
|
||||
cellWeights.forEach((weight, i) => {
|
||||
this.cellWeights.forEach((weight, i) => {
|
||||
// 係数
|
||||
const coefficient = 30;
|
||||
weight = weight * coefficient;
|
||||
|
||||
const stone = o.board[i];
|
||||
if (stone === botColor) {
|
||||
const stone = this.o.board[i];
|
||||
if (stone === this.botColor) {
|
||||
// TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する
|
||||
score += weight;
|
||||
} else if (stone !== null) {
|
||||
|
@ -188,30 +215,37 @@ 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;
|
||||
}
|
||||
|
||||
private think = () => {
|
||||
console.log('Thinking...');
|
||||
console.time('think');
|
||||
|
||||
// 接待モードのときは、全力(5手先読みくらい)で負けるようにする
|
||||
const maxDepth = this.isSettai ? 5 : this.strength;
|
||||
|
||||
/**
|
||||
* αβ法での探索
|
||||
*/
|
||||
const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
|
||||
// 試し打ち
|
||||
o.put(o.turn, pos);
|
||||
this.o.put(this.o.turn, pos);
|
||||
|
||||
const key = o.board.toString();
|
||||
const key = this.o.board.toString();
|
||||
let cache = db[key];
|
||||
if (cache) {
|
||||
if (alpha >= cache.upper) {
|
||||
o.undo();
|
||||
this.o.undo();
|
||||
return cache.upper;
|
||||
}
|
||||
if (beta <= cache.lower) {
|
||||
o.undo();
|
||||
this.o.undo();
|
||||
return cache.lower;
|
||||
}
|
||||
alpha = Math.max(alpha, cache.lower);
|
||||
|
@ -223,44 +257,44 @@ function think() {
|
|||
};
|
||||
}
|
||||
|
||||
const isBotTurn = o.turn === botColor;
|
||||
const isBotTurn = this.o.turn === this.botColor;
|
||||
|
||||
// 勝った
|
||||
if (o.turn === null) {
|
||||
const winner = o.winner;
|
||||
if (this.o.turn === null) {
|
||||
const winner = this.o.winner;
|
||||
|
||||
// 勝つことによる基本スコア
|
||||
const base = 10000;
|
||||
|
||||
let score;
|
||||
|
||||
if (game.settings.isLlotheo) {
|
||||
if (this.game.settings.isLlotheo) {
|
||||
// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
|
||||
score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100);
|
||||
score = this.o.winner ? base - (this.o.blackCount * 100) : base - (this.o.whiteCount * 100);
|
||||
} else {
|
||||
// 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する
|
||||
score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100);
|
||||
score = this.o.winner ? base + (this.o.blackCount * 100) : base + (this.o.whiteCount * 100);
|
||||
}
|
||||
|
||||
// 巻き戻し
|
||||
o.undo();
|
||||
this.o.undo();
|
||||
|
||||
// 接待なら自分が負けた方が高スコア
|
||||
return isSettai
|
||||
? winner !== botColor ? score : -score
|
||||
: winner === botColor ? score : -score;
|
||||
return this.isSettai
|
||||
? winner !== this.botColor ? score : -score
|
||||
: winner === this.botColor ? score : -score;
|
||||
}
|
||||
|
||||
if (depth === maxDepth) {
|
||||
// 静的に評価
|
||||
const score = staticEval();
|
||||
const score = this.staticEval();
|
||||
|
||||
// 巻き戻し
|
||||
o.undo();
|
||||
this.o.undo();
|
||||
|
||||
return score;
|
||||
} else {
|
||||
const cans = o.canPutSomewhere(o.turn);
|
||||
const cans = this.o.canPutSomewhere(this.o.turn);
|
||||
|
||||
let value = isBotTurn ? -Infinity : Infinity;
|
||||
let a = alpha;
|
||||
|
@ -282,7 +316,7 @@ function think() {
|
|||
}
|
||||
|
||||
// 巻き戻し
|
||||
o.undo();
|
||||
this.o.undo();
|
||||
|
||||
if (value <= alpha) {
|
||||
cache.upper = value;
|
||||
|
@ -299,7 +333,7 @@ function think() {
|
|||
}
|
||||
};
|
||||
|
||||
const cans = o.canPutSomewhere(botColor);
|
||||
const cans = this.o.canPutSomewhere(this.botColor);
|
||||
const scores = cans.map(p => dive(p));
|
||||
const pos = cans[scores.indexOf(Math.max(...scores))];
|
||||
|
||||
|
@ -310,4 +344,38 @@ function think() {
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Session();
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"noEmitOnError": true,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": false,
|
||||
|
|
Loading…
Reference in a new issue