8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

VueAdvent Calendar 2022

Day 22

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

Last updated at Posted at 2022-12-19

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にプラグインの追加

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>

コンポーネント作成

コンポーネントを2つ作ります。一つは 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.コンピュータ囲碁講習会

コンピュータ囲碁・モンテカルロ法の理論と実践を参考に作成し、途中で、電気通信大学 コンピューター囲碁講習会 実践編1回 を参照しています。変数名や関数名は、できるだけ実践編のほうに片寄せしています。資料やサンプルコードはコンピュータ囲碁講習会からたどって行けます。

実践編1

Piniaでゲーム管理環境を作る。

グリッドは1から始まるが、配列は0からなので計算が発生しないように番号を合わせています。コミは7目で、中国ルール。盤面データは、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;
};

ここまでで力尽きて時間切れです。さらなる改良のため参考文献をあげていきます。

参考文献

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?