vue+electronで囲碁の開発環境を




# 1.開発環境構築までの流れ
## 動機

最近、囲碁クエストの九路盤にハマったので、九路盤に関する書籍を読み漁りました。
kindle Unlimitedで読めるのは下記。

- 9路盤で勝つ21の必勝戦法
- 9路盤完全ガイド
- 9路盤の手筋

然し、小生は囲碁初心者(囲碁クエスト9級)なので、棋譜を並べるのがなれないのと面倒だし、盤面の途中からどこから打つのか惑うばかりなで、定石研究にソフトの力を借りれないものかとなりました。 

## なぜVueなのか？
なぜか、Vue Masteryのsubscriptionを持っていて、ほったらかしにしていたのが来年の３月が期限を迎えます。もったいないので、期限が来ない内に、ソフトを１本書こうかなと思い立ちました。VuexがPiniaになったしね。[2022年の最新標準！Vue 3の新しい開発体験に触れよう](https://ics.media/entry/220120/)の[Vue 3.2の新しい推薦構成で実装した◯×ゲームのサンプルコード](https://github.com/ics-creative/220120_vue3_stack/tree/master/vue3-sample)は参考にさせてもらいました。3x3を9x9にしただけだからね。

## なぜelectronなのか？
前に、react+electronでソフトを書こうとして挫折しました。reactは小生には向いていなかったようです。vue+electronの記事が数件でているので、それを参考にして楽しようと思っていました。最初、[vite(+vue)+electronでアプリをビルドする](https://qiita.com/Quantum/items/00fe28792bb869aa4f65)を参考に環境構築を始めたのですが、最新が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にプラグインの追加

``` vite.config.ts
    base './',
    plugins:[vue(),vueJsx(), electron({ entry: "electron/main.ts" })], ...
```

## 開発環境を動かす
以下のコマンド一発で、httpサーバとelectronが起動します。
electron側で終了させると、バックグラウンドも落ちます。便利！
```
$ yarn dev
```


# 3.九路盤の表示

九路盤を表示するためのソースを追加して行きます。
## ページの追加
```router/index.ts
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;
```
## 表示部作成
```views/GameView.vue
<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>
```
## コンポーネント作成
コンポーネントを２つ作ります。一つは views/GameView.vue にある碁盤(kyuroBan)、もう一つは、碁盤のマス目(kyuroCell)です。
碁盤は親で、碁盤の升目は子の関係になります。親から子へのpropsでのデータ渡しは、セル番号だけです。
子から親へEmitが必要なのかとemitをコーディングしてましたが、必要ありませんでした。

```kyuroBan.vue
<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>
```

```kyuroCell.vue
<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の値を変更すると盤面の表示も変化します。

```logic/placeStone.ts
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.コンピュータ囲碁講習会
[コンピュータ囲碁・モンテカルロ法の理論と実践](https://www.kyoritsu-pub.co.jp/book/b10007174.html)を参考に作成し、途中で、[電気通信大学 コンピューター囲碁講習会 実践編１回](https://www.youtube.com/watch?v=kWlOrPZjEgQ&t=3355s) を参照しています。変数名や関数名は、できるだけ実践編のほうに片寄せしています。資料やサンプルコードは[コンピュータ囲碁講習会](http://entcog.c.ooco.jp/entcog/cg_koushu.html#1)からたどって行けます。
## 実践編1
### Piniaでゲーム管理環境を作る。

グリッドは1から始まるが、配列は0からなので計算が発生しないように番号を合わせています。コミは７目で、中国ルール。盤面データは、0..空  1..黒  2..白  3..壁 です。
最初、盤面データをいじっても、画面表示に反映されませんでしたが、バグが取れると、急に動くようになりました。stores/game.ts で定義している関数は、石を盤面に置くutuと初期画面にするstartだけです。注意点は、配列を使う場合は、splice関数を使えということでしょうか。

```stores/game.ts
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 };
});
```
### 共通関数
```logic/common.ts
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の石を無条件に取り除きます。

```logic/move.ts
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);
  }
};
```

### プレイアウト
このプログラムでは、セキに対して平気で自爆手を打つので、改良が必要です。


``` logic/playout.ts
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);
};
```






### 勝敗の判定
```logic/playout.ts
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;
};
```
ここまでで力尽きて時間切れです。さらなる改良のため参考文献をあげていきます。


# 参考文献

- [電気通信大学 コンピューター囲碁講習会 実践編２回](https://www.youtube.com/watch?v=MZMov82KjP8&t=2409s)
- [強豪囲碁ソフト「Ray」のソースコード](https://github.com/kobanium/Ray)
- [Rayn の技術と自戦記](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwjW-OqWxc_7AhXvglYBHYR-CpQQFnoECBAQAQ&url=https%3A%2F%2Fwww.jstage.jst.go.jp%2Farticle%2Fjjsai%2F32%2F5%2F32_765%2F_pdf&usg=AOvVaw1o93n4c7QsbHFv-KHC_FD8)
- [Rn(九路盤専用)](https://github.com/zakki/leela-zero/releases)