以下の内容はhttps://smooth-pudding.hatenablog.com/entry/2025/05/26/221714より取得しました。


【AI×Tauri】第五回:ボードのフロントエンド部分を作る Part 2

前回はボードの実装の助走として、キャンバス上のクリックした場所にコマを配置する機能を実装しました。
smooth-pudding.hatenablog.com

今回はボード上の丸印の上にコマを積む機能を実装していきたいと思います。

積む場所を固定する

前回は自由な位置にコマを配置できる機能をつくりましたが、実際のゲームではボード上の丸の書かれた場所の上にしか置きません。つまり、コマを配置する場所は実質8箇所×3段の24通りしかありません。

また、一番下のコマを置く座標を特定すれば、二段目と三段目の座標は定数でずらせば求まるはずです。つまり

  • 一番下のコマの置く場所8個
  • 一段上がるときの y 座標の変化幅

を求めておけば、すべての場所を表現できます。

さらに、ボードは上下左右に対称性があります。中心の位置を基準としておけば、右上四半の座標2つさえわかれば、あとは符号の入れ替えですべて表現できます。つまり

  • 一番下のコマを置く位置2個 (中心基準座標)
  • 一段上がるときの y 座標の変化幅

を求めておけばOKです。

ところで、ボードの横幅やコマの横幅は、あとで微調整するかもしれません。であれば、一番下のコマの位置はボードの横幅に対する倍率で、一段上がるときの y 座標の変化幅はコマの横幅に対する倍率で求めておくとよさそうです。

各種定数を求める

ここまでの議論で、以下5つの定数を求めれば良いことが分かりました。

  • 2つの座標、合計4つ
  • 一段上がる y 座標の増加幅

これらは実験的に求める他ないので、実験のための仕様に一度実装変更してもらいます。Board.tsx を開いた状態で Agent を開き、以下のように命令します。

canvas 上をクリックしたときの挙動を以下に変更してください。

  • ある x, y, dy という Number 型の定数を定義しておく
  • 一回目のクリックでは (x, y) にコマを配置する
  • 二回目のクリックでは (x, y + dy) にコマを配置する
  • 以降、n 回目のクリックでは (x, y + n*dy) にコマを配置する
  • ただし以上の座標は canvas の中心を (0, 0) とする

すると以下の実装を用意してくれました。直観的でわかりやすい実装です。


▼変更部分の抜粋をクリックで表示
実験してわかったのですが、x座標は左端が0で右に行くほど大きい数値、y座標は上端が0で下に行くほど大きい数値のようです。したがって dy は負の数になりそうです。

コマの横幅やボードの幅の倍率で表現するために、すこし修正して以下のようにします。

const relativeX = x * canvas.width;
const relativeY = y * canvas.width + (currentClick - 1) * dy * pieceWidth;

気合で数値を動かして実験した結果、以下が求まりました。

const [x1, y1] = [0.137, 0.248];
const [x2, y2] = [0.330, 0.095];
const dy = -0.19;
二箇所に三段ずつ置いてみた図

これを踏まえて、以下のような関数を作っておきます。posIndex は上の右側にあるマスを 0 として時計回りに一周する向きに増えるインデックスで、heightIndex は下から順に 0, 1, 2 となるインデックスです。

const calcPieceCoordinate = (canvas: HTMLCanvasElement, pieceWidth: number, posIndex: number, heightIndex: number) => {
  const dh = 0.19;
  const [x1, y1] = [0.137, 0.248];
  const [x2, y2] = [0.330, 0.095];

  const [xBase, yBase] = [
    [x1, -y1], [x2, -y2], [x2, y2], [x1, y1],
    [-x1, y1], [-x2, y2], [-x2, -y2], [-x1, -y1],
  ][posIndex];

  const relativeX = xBase * canvas.width;
  const relativeY = yBase * canvas.width - dh * heightIndex * pieceWidth;

  const canvasCenterX = canvas.width / 2;
  const canvasCenterY = canvas.height / 2;

  return { x: relativeX + canvasCenterX, y: relativeY + canvasCenterY };
};

これを用いて handleCanvasClick を以下のように書き換えます。

const handleCanvasClick = (event: React.MouseEvent<HTMLCanvasElement>) => {
  const canvas = canvasRef.current;
  if (canvas) {
    // 駒の配置を計算
    const posIndex = clickCount % 8;
    const heightIndex = Math.floor(clickCount / 8);
    const { x, y } = calcPieceCoordinate(canvas, pieceWidth, posIndex, heightIndex);

    // 新しい駒を追加
    setPieces(prevPieces => [...prevPieces, { x, y }]);
    setClickCount(prevClick => prevClick + 1);
  }
};

この状態で24回クリックすればきれいに積み上がるはず!

24回クリック

・・・あるぇ〜〜???

ちょっとずれてしまいました。何が原因でしょうか・・・?

正解は、「コマの画像の中心座標は、コマの底面の中心の座標ではないから」です。上側に置くコマを上下反転させればピッタリ枠にはまるはずですが、実際はそうではないので、補正が必要です。

calcPieceCoordinate にもうひとつの定数 dh0 を導入して、y1, y2 を微調整します。dh0 もコマの幅に対する倍率として定義しておきます。

const calcPieceCoordinate = (canvas: HTMLCanvasElement, pieceWidth: number, posIndex: number, heightIndex: number) => {
  const dh0 = 0.12; // コマの底面の中心に補正する項
  const dh = 0.19; // コマの高さ
  const [x1, y1] = [0.137, 0.263];
  const [x2, y2] = [0.330, 0.110];

  const [xBase, yBase] = [
    [x1, -y1], [x2, -y2], [x2, y2], [x1, y1],
    [-x1, y1], [-x2, y2], [-x2, -y2], [-x1, -y1],
  ][posIndex];

  const relativeX = xBase * canvas.width;
  const relativeY = yBase * canvas.width - (dh0 + dh * heightIndex) * pieceWidth;

  const canvasCenterX = canvas.width / 2;
  const canvasCenterY = canvas.height / 2;

  return { x: relativeX + canvasCenterX, y: relativeY + canvasCenterY };
};

これで無事きれいに配置できました。

微調整後

コマのデータの持ち方を変更

これまでコマは描画する座標で管理してきました。しかし、これからは24箇所しか配置しないので、posIndex と heightIndex の組で持てば十分そうです。

ただ変更する箇所が結構多そうで面倒なので、Cursor に命令します。

Piece が保持するデータを以下に変更して。

  • posIndex: number
  • heightIndex: number

また、calcPieceCoordinate の3,4番目の引数の代わりに、ひとつの Piece を受け取るようにして。
その他、関連するところも修正して。

するとサラサラと編集してくれました。座標を計算するタイミングが描画のタイミングに移動しています。


▼コードをクリックで表示
最後に、黒のコマも置けるようにしましょう。Piece の中で 'B' または 'W' の値をとる color を用意します。描画の際はその値に応じて白か黒を描画するようにしてもらいましょう。以下を Agent で命令します。

以下に対応して

  • Piece に color: 'B' | 'W' の項目を追加する
  • 黒のコマ @piece_black.svg を読み込むようにする
  • Piece の color の値に応じて白または黒のコマを描画するようにする
  • クリックイベントでは clickCount の偶奇に応じて白と黒を入れ替えて

こちらもサラサラっと書き換えてくれました。


▼コードをクリックで表示
いい感じに二色のコマを置けるようになりました。

白黒に対応

第五回まとめ

8つの丸の位置を指定する値を頑張って探すことで、きれいに丸の上にコマを重ねられるようになりました。また一歩、本物のセカンドベストのボードに近づきました。

今回実装した範囲までのコードは v5-piece-on-circles というタグで push してあります。
github.com

次回は、8箇所それぞれに対してクリックイベントを実装する部分に取り組みたいと思います。

ではまた。

続き↓
smooth-pudding.hatenablog.com

追記

記事を投稿して「よっしゃできた〜」と喜びながら風呂に入っていたら、「え、てか、xBase と yBase は三角関数で書いたほうがシンプルじゃね?」と気づいたので、追加で編集します。

xBase, yBase は楕円の周上の点なので、あるパラメーター \theta を用いて以下のように表せます。

\begin{cases}R_{x} \cos\theta = x_{\mathrm{Base}}, \\ R_{y} \sin\theta = y_{\mathrm{Base}}\end{cases}
ここで R_{x}, R_{y} はそれぞれ x, y 方向の半径です。

前回ボードの画像を作ったときのコードを見てもらえば分かりますが、ボードの配置は

  • 小円を円周上に等間隔に並べて
  • y 方向に 0.8 倍して潰す

という風に作っています。したがって

R_{y} = R_{x} \cdot 0.8
が成り立ち、さらに
\theta = \theta_{0} + n \Delta \theta, \quad \Delta \theta = \dfrac{\pi}{4}
が成り立つことが分かります。

これを踏まえると、x1, y1, x2, y2 の値は

\begin{cases}R_{x} \cos{\dfrac{3\pi}{8}} = x_{1}, \\ R_{y} \sin{\dfrac{3\pi}{8}} = y_{1}, \\ R_{x} \cos{\dfrac{\pi}{8}} = x_{2}, \\ R_{y} \sin{\dfrac{\pi}{8}} = y_{2}\end{cases}
であることがわかります。\dfrac{\pi}{8} + \dfrac{3\pi}{8} = \dfrac{\pi}{2} であることを踏まえると
\cos{\dfrac{\pi}{8}} = \sin{\dfrac{3\pi}{8}}, \quad \sin{\dfrac{\pi}{8}} = \cos{\dfrac{3\pi}{8}}
が成り立つので、
x_{1} \cdot 0.8 = y_{2}, \quad x_{2} \cdot 0.8 = y_{1}
となることが分かります。

実際、目分量で頑張って求めた値は x1 = 0.137, y1 = 0.263, x2 = 0.330, y2 = 0.110 でしたが、0.137 * 0.8 = 0.1096 ≒ 0.110 および 0.330 * 0.8 = 0.264 ≒ 0.263 となっていることが分かります。

からくりがわかったので R_{x} を求めると

R_{x} = \dfrac{x_{1}}{\cos{\frac{3\pi}{8}}} \approx 0.358
となります。

これを踏まえて、xBase, yBase を定義すると、以下のようになります。

const radiusX = 0.358;
const radiusY = radiusX * 0.8;
const angle = (-3 * Math.PI / 8) + (Math.PI / 4) * piece.posIndex;
const [xBase, yBase] = [ radiusX * Math.cos(angle), radiusY * Math.sin(angle) ];

動作確認もバッチリです!

動作確認

タグの付け直しが面倒くさいので、この変更は次回の記事の範囲で織り込むことにします。




以上の内容はhttps://smooth-pudding.hatenablog.com/entry/2025/05/26/221714より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14