1.開発環境構築までの流れ
動機
最近、囲碁クエストの九路盤にハマったので、九路盤に関する書籍を読み漁りました。
kindle Unlimitedで読めるのは下記。
- 9路盤で勝つ21の必勝戦法
- 9路盤完全ガイド
- 9路盤の手筋
然し、小生は囲碁初心者(囲碁クエスト9級)なので、棋譜を並べるのがなれないのと面倒だし、盤面の途中からどこから打つのか惑うばかりなで、定石研究にソフトの力を借りれないものかとなりました。
なぜVueなのか?
なぜか、Vue Masteryのsubscriptionを持っていて、ほったらかしにしていたのが来年の3月が期限を迎えます。もったいないので、期限が来ない内に、ソフトを1本書こうかなと思い立ちました。VuexがPiniaになったしね。2022年の最新標準!Vue 3の新しい開発体験に触れようのVue 3.2の新しい推薦構成で実装した◯×ゲームのサンプルコードは参考にさせてもらいました。3x3を9x9にしただけだからね。
なぜelectronなのか?
前に、react+electronでソフトを書こうとして挫折しました。reactは小生には向いていなかったようです。vue+electronの記事が数件でているので、それを参考にして楽しようと思っていました。最初、vite(+vue)+electronでアプリをビルドするを参考に環境構築を始めたのですが、最新がvite2からvite3になっていたので、vite3に変えると、そのままでは、少し加工しないとエラーで動かないことに。調べていくとvite-plugin-electronがなかなか使えそうなので入れてみます。
2.構築手順
vite3を使った構築
下記のコマンドで始めます。オプションは、とりあえず ALL YESです。
$ yarn create vite
? Project name: » igovue
√ Project name: ... igovue
√ Select a framework: » Vue
√ Select a variant: » Customize with create-vue ↗
√ Add TypeScript? ... Yes
√ Add JSX Support? ... Yes
√ Add Vue Router for Single Page Application development? ... Yes
√ Add Pinia for state management? ... Yes
√ Add Vitest for Unit Testing? ... Yes
√ Add an End-to-End Testing Solution? » Cypress
√ Add ESLint for code quality? ... Yes
√ Add Prettier for code formatting? ... Yes
追加パッケージ
$ yarn add -D electron vite-plugin-electron
$ yarn add -D npm-run-all
$ yarn add -D sass autoprefixer
electronの起動ファイルを作る。
$ cd igovue
$ mkdir electron
$ cd electron
electron用のディレクトリを用意し、main.ts と preload.js を作成します。
main.tsなのに、requireを使っているのは、動けば良しという考え。
vite3からポートが5173に替わっているので注意してね。
preload.jsは取り合えずはファイルだけ作って空にしています。
const { app, BrowserWindow } = require("electron");
const path = require("path");
const isDevelopment = ("" + process.env.NODE_ENV).trim() === "development";
function createWindow() {
const win = new BrowserWindow({
width: 1600,
height: 1000,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
});
win.loadURL("http://localhost:5173/"); // ポート番号がvite3になって3000から変わっています。
}
app.whenReady().then(() => {
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
エントリポイントの変更
$ yarn init
entry point : electron/main.ts
vite.config.tsにプラグインの追加
base './',
plugins:[vue(),vueJsx(), electron({ entry: "electron/main.ts" })], ...
開発環境を動かす
以下のコマンド一発で、httpサーバとelectronが起動します。
electron側で終了させると、バックグラウンドも落ちます。便利!
$ yarn dev
3.九路盤の表示
九路盤を表示するためのソースを追加して行きます。
ページの追加
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ ... },
{
path: "/game",
name: "game",
component: () => import("../views/GameView.vue"),
},
{ ... },
],
});
export default router;
表示部作成
<script setup lang="ts">
import kyuroBan from "../components/kyuroBan.vue";
</script>
<template>
<div class="game">
<kyuroBan />
</div>
</template>
<style>
@media (min-width: 2048px) {
.game {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>
コンポーネント作成
コンポーネントを2つ作ります。一つは views/GameView.vue にある碁盤(kyuroBan)、もう一つは、碁盤のマス目(kyuroCell)です。
碁盤は親で、碁盤の升目は子の関係になります。親から子へのpropsでのデータ渡しは、セル番号だけです。
子から親へEmitが必要なのかとemitをコーディングしてましたが、必要ありませんでした。
<script setup lang="ts">
import kyuroCell from "./kyuroCell.vue";
import { useGameStore } from "@/stores/game";
import { playoutRun } from "@/logic/playout";
const game = useGameStore();
const Run = (): void => {
playoutRun();
};
const start = (): void => {
game.start();
console.log("tekazu = %d", game.tekazu);
};
</script>
<template>
<div>
九路盤 手数:{{ game.tekazu }} 手番: {{ game.teban }} <br />
<div class="GameBoard">
<div class="board">
<div class="cell" v-for="cellNo in 121" :key="cellNo">
<kyuroCell :No="cellNo" />
</div>
</div>
</div>
</div>
<button id="start" @click="start">初期化</button>
<button id="run" @click="Run">プレイアウト</button><br />
<div></div>
棋譜 <div class="kifu">{{ game.kifu.join() }}</div>
</template>
<style lang="scss" scoped>
.GameBoard {
width: 567px;
height: 567px;
background-image: url(/九路盤.png);
.board {
display: grid;
grid-template-columns: 8px 60px 60px 60px 60px 60px 60px 60px 60px 60px 3px;
grid-template-rows: 1px 60px 60px 60px 60px 60px 60px 60px 60px 60px 3px;
gap: 1px;
}
}
.kifu {
width: 200px;
overflow-wrap: normal;
}
</style>
<script setup lang="ts">
import { placeStone } from "@/logic/placeStone";
import { move, takeStone, countLiberty } from "@/logic/move";
import { useGameStore } from "@/stores/game";
const props = defineProps<{
No: number;
}>();
const game = useGameStore();
const no = props.No;
const clickCell = () => {
const status = game.board[no];
if (status == 0) {
move(no);
} else {
const p = countLiberty(no);
console.log("tz=%d librtyy=%d stone=%d", no, p.liberty, p.stone);
}
};
const clickRight = () => {
const color = game.board[no];
console.log("no = %d color = %d", no, color);
if (color == 1 || color == 2) {
takeStone(no, color);
}
};
</script>
<template>
<div class="kyuroCell" @click="clickCell" @click.right="clickRight">
<div class="stone">
{{ placeStone(no) }}
</div>
</div>
</template>
<style lang="scss" scoped>
.kyuroCell {
width: 100%;
height: 100%;
// border: 1px solid gray;
.stone {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
font-size: 60px;
}
}
</style>
石を表示する関数
game.boardの値を変更すると盤面の表示も変化します。
import { useGameStore } from "@/stores/game";
const game = useGameStore();
export const placeStone = (no: number): string => {
if (no == 1 || no > 109 || no < 11 || no % 11 == 0 || no % 11 == 1) return "";
const status = game.board[no];
if (status == 1) return "\u26AB";
if (status == 2) return "\u26AA";
return "";
};
4.コンピュータ囲碁講習会
コンピュータ囲碁・モンテカルロ法の理論と実践を参考に作成し、途中で、電気通信大学 コンピューター囲碁講習会 実践編1回 を参照しています。変数名や関数名は、できるだけ実践編のほうに片寄せしています。資料やサンプルコードはコンピュータ囲碁講習会からたどって行けます。
実践編1
Piniaでゲーム管理環境を作る。
グリッドは1から始まるが、配列は0からなので計算が発生しないように番号を合わせています。コミは7目で、中国ルール。盤面データは、0..空 1..黒 2..白 3..壁 です。
最初、盤面データをいじっても、画面表示に反映されませんでしたが、バグが取れると、急に動くようになりました。stores/game.ts で定義している関数は、石を盤面に置くutuと初期画面にするstartだけです。注意点は、配列を使う場合は、splice関数を使えということでしょうか。
import { ref } from "vue";
import { defineStore } from "pinia";
import { flipColor } from "@/logic/common";
export const shokiBanmen = [
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
].flat();
export const useGameStore = defineStore("game", () => {
const komi = ref(7);
const teban = ref(1); // 手番
const tekazu = ref(0);
const kifu = ref<number[]>([]);
const koZ = ref(0); // コウで打てない位置
const arr2 = shokiBanmen.concat();
const board = ref(arr2);
const utu = (no: number): void => {
board.value.splice(no, 1, teban.value);
kifu.value.push(no);
tekazu.value++;
teban.value = flipColor(teban.value);
};
const start = (): void => {
teban.value = 1;
tekazu.value = 0;
kifu.value.length = 0;
board.value.splice(0, arr2.length, ...arr);
};
return { komi, teban, tekazu, kifu, koZ, all_playout, board, utu, start };
});
共通関数
export const pMin: number = 13; // 盤面検索Min
export const pMax: number = 110; // 盤面検索Max
export const flipColor = (color: number): number => {
return 3 - color;
};
石を打つ関数
javascriptには、参照渡しがないので、オブジェクトもしくは、Piniaでデータを渡しています。
countLibertyは、ダメ数と、連結する石数を数える関数です。
moveは、tzに石を置く関数で、着手禁止点とコウを判断し、着手可能なら石を置きます。
takeStoneは、tzの位置と連結するcolorの石を無条件に取り除きます。
import { useGameStore } from "@/stores/game";
import { pMax, flipColor } from "@/logic/common";
const game = useGameStore();
const checkBoard = Array(pMax);
export type TL = { liberty: number; stone: number };
export const dir4 = [1, -11, -1, 11];
const countLibertySub = (tz: number, color: number, p: TL): void => {
p.stone++;
checkBoard[tz] = 1;
for (let i = 0; i < 4; i++) {
const z = tz + dir4[i];
if (checkBoard[z] == 1) continue;
if (game.board[z] == 0) {
checkBoard[z] = 1;
p.liberty++;
}
if (game.board[z] == color) countLibertySub(z, color, p);
}
};
export const countLiberty = (tz: number): TL => {
const p: TL = { liberty: 0, stone: 0 };
checkBoard.fill(0);
countLibertySub(tz, game.board[tz], p);
return p;
};
export const move = (tz: number): number => {
const color = game.teban;
if (tz == 0) {
game.kifu.push(0);
game.teban = flipColor(color);
game.koZ = 1;
return 0;
}
const unCol: number = flipColor(color);
let space: number = 0;
let kabe = 0;
let mikataSafe = 0;
let takeSum = 0;
let koKamo = 0;
const around = [
{ liberty: 0, stone: 0, color: 0 },
{ liberty: 0, Store: 0, color: 0 },
{ liberty: 0, stone: 0, color: 0 },
{ liberty: 0, stone: 0, color: 0 },
];
for (let i = 0; i < 4; i++) {
around[i].liberty = 0;
around[i].stone = 0;
around[i].color = 0;
const z = tz + dir4[i];
const c = game.board[z];
if (c == 0) space++;
if (c == 3) kabe++;
if (c == 0 || c == 3) continue;
const p = countLiberty(z);
around[i].liberty = p.liberty;
around[i].stone = p.stone;
around[i].color = c;
if (c == unCol && p.liberty == 1) {
takeSum += p.stone;
koKamo = z;
}
if (c == color && p.liberty >= 2) mikataSafe++;
}
if (takeSum == 0 && space == 0 && mikataSafe == 0) return 1;
if (tz == game.koZ) return 2;
if (kabe + mikataSafe == 4) return 3;
if (game.board[tz] != 0) return 4;
for (let i = 0; i < 4; i++) {
const z = tz + dir4[i];
const d = around[i].liberty;
const c = around[i].color;
if (c == unCol && d == 1) {
takeStone(z, unCol);
}
}
game.utu(tz);
const p = countLiberty(tz);
if (takeSum == 1 && p.liberty == 1 && p.stone == 1) {
game.koZ = koKamo;
} else {
game.koZ = 0;
}
return 0;
};
export const takeStone = (tz: number, color: number): void => {
game.board.splice(tz, 1, 0);
for (let i = 0; i < 4; i++) {
const z = tz + dir4[i];
if (game.board[z] == color) takeStone(z, color);
}
};
プレイアウト
このプログラムでは、セキに対して平気で自爆手を打つので、改良が必要です。
export const playoutRun = (): void => {
playout.allPlayout++;
const turnColor = game.teban;
let color = turnColor;
playout.beforeZ = -1;
for (let loop = 0; loop < loopMax; loop++) {
for (let i = pMin; i < pMax; i++) {
if (game.board[i] == 0) playout.kouho.push(i);
}
for (;;) {
const m = playout.kouho.length;
if (playout.kouho.length == 0) {
z = 0;
} else {
r = Math.floor(Math.random() * m);
z = playout.kouho[r];
}
const err = move(z);
if (err == 0) break;
playout.kouho.splice(r, 1);
}
if (z == 0 && playout.beforeZ == 0) break;
playout.beforeZ = z;
color = flipColor(color);
}
const win = countScore();
console.log("win = %d", win);
};
勝敗の判定
const countScore = (): number => {
let black: number = 0;
let white: number = 0;
for (let i = pMin; i < pMax; i++) {
const status = game.board[i];
if (status == 1) black++;
if (status == 2) white++;
if (status == 0) {
for (let j = 0; j < 4; j++) {
const z = i + dir4[j];
const around = game.board[z];
if (around == 1) black++;
if (around == 2) white++;
if (around == 1 || around == 2) break;
}
}
}
console.log("Black = %d White = %d komi = %d", black, white, game.komi);
if (black > white + game.komi) return 1;
if (black < white + game.komi) return 2;
return 0;
};
ここまでで力尽きて時間切れです。さらなる改良のため参考文献をあげていきます。