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;
};
ここまでで力尽きて時間切れです。さらなる改良のため参考文献をあげていきます。