見出し画像

GPT-4Vのマルチモーダル機能を利用してオセロゲームを生成させた話

目次


本記事の概要


  • GPT-4Vのマルチモーダル機能(画像入力)のゲーム開発への応用能力に関する知識共有を目的に、この画像入力機能を利用しながらオセロゲームを開発しました。

  • その結果、類似のオセロの盤面デザインを迅速に再現することができました。

  • この結果から、画像入力機能を利用することで、類似のゲームデザインの再現時に生産性が向上する可能性があると結論付けました。

本記事の作成目的


GPT-4Vのマルチモーダル機能(画像認識)のゲーム開発への応用能力の知識共有

  • 画像認識機能を利用して、GPT-4Vに既存のオセロゲームの盤面を再現させたり、バグ画像を分析させたりしながら、オセロゲームを開発しました。

  • このプロセスを通じて得られた、画像認識機能の精度や、生産性向上に関する著者の所感などを共有することを目的とします。

GPT-4Vについて


  • OpenAIによって開発されたマルチモーダルAIであり、テキスト、音声、画像、動画、数値などのデータ解析と出力を行えます。

  • 2023年9月25日に公開されました。

  • GPT-4Vの「V」はVisionを指しています。

実行環境


  1. 利用プログラミング言語

    • javascript, html, css

  2. 利用マルチモーダル機能

    • 画像認識

  3. ゲームエンジン

    • 未使用

生成ゲーム


  • 上の動画の冒頭が完成したオセロゲームのプレイ映像となります。

  • プレイ映像以降はGPT-4Vとの全てのチャット内容となります。

盤面デザインの再現


チャット

  • USER

下のプロント入力時に読み込ませた画像
(以降、プロンプト上部の画像は全て、下部のプロンプト入力時に読み込ませた画像とします。)
 このゲームを再現してください。
  • ASSISTANT

了解しました。このゲームは「オセロ」として知られています。以下に基本的なルールを示します。

1. ゲームは8x8の盤上で行われますが、提供された画像はもう少し簡単な盤を使用しているようです。
2. プレイヤーは交互に盤上に石を置きます。石には黒と白の2種類があります。
3. 石を置くと、その石の置かれた位置から直線的に敵の石を挟むことができる場合、挟まれた敵の石を自分の色に変えることができます。
4. 盤上に石を置ける場所がなくなった時点でゲーム終了。多くの石を持っているプレイヤーが勝者となります。

提供された画像を基にゲームを再現するには、このルールに従い、次の手番のプレイヤーが石を置ける位置を見つけ、その位置に石を置くことで敵の石を取得するプロセスを繰り返すことになります。

具体的に何をお手伝いできるか教えていただけますか?
  • USER

あなたはjavascript、html、cssのスペシャリストです。このゲームを再現するコードを書いてください。
  • ASSISTANT

了解しました。簡単なオセロゲームを再現するための基本的なコードを提供します。以下はHTML、CSS、JavaScriptを組み合わせた例です。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>オセロゲーム</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="board">
        <!-- JavaScriptで盤面を生成します -->
    </div>
    <script src="script.js"></script>
</body>
</html>



body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f4f4f4;
}

.board {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    grid-gap: 2px;
    width: 320px;
    height: 320px;
}

.cell {
    width: 40px;
    height: 40px;
    background-color: green;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
}

.cell.black {
    background-color: black;
}

.cell.white {
    background-color: white;
}




const boardSize = 8;
const board = document.querySelector('.board');
let currentPlayer = 'black';

// 盤面の初期化
for (let i = 0; i < boardSize; i++) {
    for (let j = 0; j < boardSize; j++) {
        const cell = document.createElement('div');
        cell.classList.add('cell');
        cell.dataset.row = i;
        cell.dataset.col = j;
        board.appendChild(cell);
    }
}

// 初期配置
document.querySelector(`[data-row='${boardSize/2-1}'][data-col='${boardSize/2-1}']`).classList.add('white');
document.querySelector(`[data-row='${boardSize/2}'][data-col='${boardSize/2}']`).classList.add('white');
document.querySelector(`[data-row='${boardSize/2-1}'][data-col='${boardSize/2}']`).classList.add('black');
document.querySelector(`[data-row='${boardSize/2}'][data-col='${boardSize/2-1}']`).classList.add('black');

// TODO: ゲームのロジックを実装する(石を置く、石をひっくり返すなど)

board.addEventListener('click', (e) => {
    const cell = e.target;
    // すでに石が置かれているかチェック
    if (!cell.classList.contains('black') && !cell.classList.contains('white')) {
        cell.classList.add(currentPlayer);
        // TODO: 石をひっくり返すロジック

        // プレイヤーを交代
        currentPlayer = currentPlayer === 'black' ? 'white' : 'black';
    }
});

このコードは基本的な盤面の生成と初期配置のみを行っています。ゲームのロジック、特に石をひっくり返す部分など、さらなる実装が必要です。ただし、これを基にゲームの実装を開始することができます。
  • USER

この画像の盤面が生成されました。元の画像の盤面との違いを説明してください。
  • ASSISTANT

画像を比較すると、以下の違いが見受けられます:

1. 提供された新しい画像の盤面は、8x8の正方形のグリッドです。これは、オセロの標準的な盤面のサイズに一致しています。
2. 画像の中央に2つの石が配置されています。1つは白、もう1つは黒です。これはオセロのゲーム開始時の典型的な配置とは異なります。標準的な開始配置では、中央の4つのセルに、対角線上に白と黒の石が交互に配置されています。
3. 画像には、その他のセルに石が置かれていないことが確認できます。

元の画像の盤面との主な違いは、石の配置パターンと、使用されている盤面のサイズです。新しい画像の盤面は、ゲームの開始直後の状態とは異なる特殊な配置を持っています。
  • USER

グリッドの色と石の形が違います。

具体的には、以下の違いがあります。
- グリッドの色
    - 元画像
        - 黒
    - 本画像
        - 白
- 石の形
    - 元画像
        - 円形
    - 本画像
        - 正方形

この点を改善してください。
  • ASSISTANT

申し訳ございません。指摘していただいた点に基づいて、CSSを修正します。

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f4f4f4;
}

.board {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    grid-gap: 1px; /* グリッドの境界線を表示するための小さな隙間 */
    width: 320px;
    height: 320px;
    background-color: black; /* グリッドの背景色を黒に設定 */
}

.cell {
    width: 40px;
    height: 40px;
    background-color: green;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    position: relative; /* 石を中央に配置するための基準点として */
}

.cell.black::after, .cell.white::after {
    content: '';
    position: absolute;
    width: 30px;
    height: 30px;
    border-radius: 50%; /* 円形にする */
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%); /* 中央に配置 */
}

.cell.black::after {
    background-color: black;
}

.cell.white::after {
    background-color: white;
}


この改善版のCSSは、盤面のグリッドの背景色を黒に設定し、石を円形に表示するように修正しています。これにより、ご指摘いただいた点が反映されたデザインになるはずです。
  • USER

元画像に、近づきましたが、以下の点が微妙に異なります。
この点を修正してください。

- グリッドの色 
    - 元画像
        - 黒一色
    - 本画像
        - 黒白混在
        - 右端と下端が白
  • ASSISTANT

指摘していただいた点をもとに、CSSをさらに修正します。

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f4f4f4;
}

.board {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    width: 322px;  /* グリッドの境界線の分を加算 */
    height: 322px; /* グリッドの境界線の分を加算 */
    background-color: black; /* グリッドの背景色を黒に設定 */
    border: 1px solid black; /* グリッドの外周を黒で囲む */
}

.cell {
    width: 40px;
    height: 40px;
    background-color: green;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    position: relative; /* 石を中央に配置するための基準点として */
    border: 1px solid black; /* 各セルの境界線を黒に設定 */
}

.cell.black::after, .cell.white::after {
    content: '';
    position: absolute;
    width: 30px;
    height: 30px;
    border-radius: 50%; /* 円形にする */
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%); /* 中央に配置 */
}

.cell.black::after {
    background-color: black;
}

.cell.white::after {
    background-color: white;
}

この再修正版のCSSでは、盤面の全体を囲む境界線と各セルの境界線を黒に設定しています。これにより、グリッドの色が元画像のように一貫して黒くなり、右端と下端の白い部分も修正されるはずです。
  • USER

ボーダーの色は修正されましたが、以下の点がおかしくなりました。
この点を修正してください。

- ボーダー
    - 本画像
        - 右と下のボーダーが欠けている
        - 右上と左下のマスのボーダーが欠けている
        - ボーダー幅がやや元画像より太い
- グリッド
    - 本画像
        - グリッド幅がやや元画像より太い
  • ASSISTANT

ご指摘いただいた内容をもとに、CSSをさらに修正いたします。


body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f4f4f4;
}

.board {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    width: 320px;  
    height: 320px; 
    background-color: black; 
}

.cell {
    box-sizing: border-box; /* ボーダーの太さをセルの外寸に含める */
    width: 40px;
    height: 40px;
    background-color: green;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    position: relative; 
    border-right: 1px solid black; 
    border-bottom: 1px solid black; 
}

/* 最右列と最下列のボーダーを修正 */
.cell:nth-child(8n) {
    border-right: none;
}
.cell:nth-last-child(-n+8) {
    border-bottom: none;
}

.cell.black::after, .cell.white::after {
    content: '';
    position: absolute;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%); 
}

.cell.black::after {
    background-color: black;
}

.cell.white::after {
    background-color: white;
}

このCSSコードは、セルの境界線の太さを修正し、右端と下端の境界線の問題を解消しています。これにより、画像に近いデザインが得られるはずです。
  • USER

グリッド幅は修正されましたが、ボーダーがなくなりました。
本画像と同じボーダーを付与してください。
  • ASSISTANT

申し訳ございません。ボーダーの問題を解消します。

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f4f4f4;
}

.board {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    width: 320px;
    height: 320px;
    background-color: black; 
    border: 1px solid black; /* ボード全体のボーダー */
}

.cell {
    box-sizing: border-box;
    width: 40px;
    height: 40px;
    background-color: green;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    position: relative; 
    border-right: 1px solid black; 
    border-bottom: 1px solid black; 
}

/* 最右列のボーダーを修正 */
.cell:nth-child(8n) {
    border-right: 1px solid black; /* 元に戻す */
}

/* 最下列のボーダーを修正 */
.cell:nth-last-child(-n+8) {
    border-bottom: 1px solid black; /* 元に戻す */
}

.cell.black::after, .cell.white::after {
    content: '';
    position: absolute;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%); 
}

.cell.black::after {
    background-color: black;
}

.cell.white::after {
    background-color: white;
}

このCSSコードでは、各セルの右端と下端にボーダーを再度追加しました。これにより、本画像のデザインに近づくはずです。再度、確認をお願いいたします。

再現結果

最終的な盤面の再現画像
元画像
  • 比較

盤面の再現画像はほぼ元画像に近いですが、盤面の右側と下側のボーダーの幅が少し太く、また石の大きさが少し大きいといった微細な違いがあります。

所感

GPT-4Vは、類似のデザインであればサクッと実装してくれるようです。
しかし、1ピクセルの誤差もないような同一のデザインを再現させることは難しいという印象があります。
同一のデザインを再現したい場合は、GPT-4Vが生成した類似のデザインを基に、人間が手動で微調整を行うのが効率的だと思います。

石をひっくり返すロジックの実装


チャット

  • USER

盤面のデザインの改修はもう良いです。
石をひっくり返すロジックを実装してください。
  • ASSISTANT

了解しました。石をひっくり返すロジックをJavaScriptで実装いたします。

以下は、石をひっくり返す基本的なロジックの実装です。8方向(上、下、左、右、左上、右上、左下、右下)に対して、現在のプレイヤーとは異なる色の石が続いているかを確認し、その後に現在のプレイヤーと同じ色の石がある場合に、その間の石をひっくり返します。

const cells = document.querySelectorAll('.cell');
let currentPlayer = 'black';  // 初期のプレイヤーを黒に設定

cells.forEach(cell => {
    cell.addEventListener('click', () => {
        // すでに石が置かれている場合は何もしない
        if (!cell.classList.contains('black') && !cell.classList.contains('white')) {
            cell.classList.add(currentPlayer);
            flipStones(cell);
            currentPlayer = currentPlayer === 'black' ? 'white' : 'black';
        }
    });
});

function flipStones(cell) {
    const directions = [
        [-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]
    ];

    directions.forEach(dir => {
        let x = cell.cellIndex + dir[0];
        let y = cell.parentElement.rowIndex + dir[1];
        const stonesToFlip = [];

        while (x >= 0 && x < 8 && y >= 0 && y < 8) {
            const currentCell = document.querySelector(`tr:nth-child(${y + 1}) td:nth-child(${x + 1})`);

            if (!currentCell.classList.contains('black') && !currentCell.classList.contains('white')) {
                break;
            }

            if (currentCell.classList.contains(currentPlayer)) {
                stonesToFlip.forEach(stone => stone.classList.replace(oppositePlayer(currentPlayer), currentPlayer));
                break;
            }

            stonesToFlip.push(currentCell);
            x += dir[0];
            y += dir[1];
        }
    });
}

function oppositePlayer(player) {
    return player === 'black' ? 'white' : 'black';
}

これで石をひっくり返すロジックが実装されました。セルをクリックすると、そのセルに現在のプレイヤーの石が置かれ、必要に応じて周囲の石がひっくり返されます。クリックするたびにプレイヤーが切り替わります。
  • USER

石をひっくり返すロジックの実装結果1
画像は実装結果となります。以下の点を修正してください。

- 石がひっくり返らない
- 石のない場所なら、どこでも石が置ける
  • ASSISTANT

了解しました。指摘された問題を修正して、JavaScriptのロジックを再構築します。

以下のロジックは以下の仕様を満たすためのものです:

1. 石がひっくり返るようにする。
2. 既に石が置かれている場所以外のどこにでも石を置けるようにする。

const cells = document.querySelectorAll('.cell');
let currentPlayer = 'black';

cells.forEach(cell => {
    cell.addEventListener('click', function() {
        if (!this.classList.contains('black') && !this.classList.contains('white')) {
            this.classList.add(currentPlayer);
            
            if (isValidMove(this)) {
                flipStones(this);
                currentPlayer = currentPlayer === 'black' ? 'white' : 'black';
            } else {
                this.classList.remove(currentPlayer);
            }
        }
    });
});

function isValidMove(cell) {
    const directions = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]];
    for (let dir of directions) {
        if (willFlipStones(cell, dir)) {
            return true;
        }
    }
    return false;
}

function willFlipStones(cell, dir) {
    let x = cell.cellIndex + dir[0];
    let y = cell.parentElement.rowIndex + dir[1];
    
    if (outOfBounds(x, y)) return false;
    
    const nextCell = document.querySelector(`tr:nth-child(${y + 1}) td:nth-child(${x + 1})`);
    if (!nextCell.classList.contains(oppositePlayer(currentPlayer))) return false;

    while (!outOfBounds(x, y)) {
        const currentCell = document.querySelector(`tr:nth-child(${y + 1}) td:nth-child(${x + 1})`);
        if (currentCell.classList.contains(currentPlayer)) {
            return true;
        }
        x += dir[0];
        y += dir[1];
    }
    return false;
}

function outOfBounds(x, y) {
    return x < 0 || x >= 8 || y < 0 || y >= 8;
}

function flipStones(cell) {
    const directions = [[-1, 0], [1, 0], [0, -
    const directions = [
        [-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]
    ];
    
    for (let dir of directions) {
        let x = cell.cellIndex + dir[0];
        let y = cell.parentElement.rowIndex + dir[1];
        const stonesToFlip = [];
        
        while (!outOfBounds(x, y)) {
            const currentCell = document.querySelector(`tr:nth-child(${y + 1}) td:nth-child(${x + 1})`);
            
            if (currentCell.classList.contains(currentPlayer)) {
                stonesToFlip.forEach(stone => stone.classList.replace(oppositePlayer(currentPlayer), currentPlayer));
                break;
            } else if (!currentCell.classList.contains(oppositePlayer(currentPlayer))) {
                break;
            }
            
            stonesToFlip.push(currentCell);
            x += dir[0];
            y += dir[1];
        }
    }
}

function oppositePlayer(player) {
    return player === 'black' ? 'white' : 'black';
}

上記のロジックでは、石が置かれたセルに対して8方向を確認して、その方向に石がひっくり返るかどうかを確認します。石がひっくり返る条件が満たされた場合、その方向にある石をすべてひっくり返します。

これにより、ユーザーがセルをクリックすると、正しく石がひっくり返る動作が実現されるはずです。
  • USER

下記エラーが発生しました。「Uncaught DOMException: Failed to execute 'querySelector' on 'Document': 'tr:nth-child(NaN) td:nth-child(NaN)' is not a valid selector.
    at willFlipStones (file:///Users/miya/program/app/chatGPTGame/41_gpt-4V_game_Othello/src/7/script.js:105:31)
    at isValidMove (file:///Users/miya/program/app/chatGPTGame/41_gpt-4V_game_Othello/src/7/script.js:92:13)
    at HTMLDivElement.<anonymous> (file:///Users/miya/program/app/chatGPTGame/41_gpt-4V_game_Othello/src/7/script.js:79:17)
willFlipStones @ script.js:105
isValidMove @ script.js:92
(匿名) @ script.js:79
  • ASSISTANT

申し訳ございません。このエラーは、`querySelector`の中で`NaN`(数値でない)を使用したために発生します。

具体的には、`willFlipStones`関数の中で、`x`および`y`の値が適切に計算・更新されていないために発生する可能性があります。

まず、`willFlipStones`関数の最初に`x`および`y`の初期値を確認し、それが数値であることを確認します。

function willFlipStones(cell, dir) {
    let x = cell.cellIndex + dir[0];
    let y = cell.parentElement.rowIndex + dir[1];
    
    console.log(x, y);  // デバッグのためのログ出力
    ...
}

上記の`console.log`の出力が数値であるかどうかを確認します。もし`NaN`が出力された場合、問題の原因はそれより前の段階で発生しています。

原因を特定するために、次のように`cell.cellIndex`および`cell.parentElement.rowIndex`の値も確認します。

function willFlipStones(cell, dir) {
    console.log(cell.cellIndex, cell.parentElement.rowIndex);  // デバッグのためのログ出力
    let x = cell.cellIndex + dir[0];
    let y = cell.parentElement.rowIndex + dir[1];
    
    console.log(x, y);  // デバッグのためのログ出力
    ...
}

これにより、どの部分で`NaN`が発生しているかのヒントが得られます。それに基づいて、エラーの原因を特定し、修正を行ってください。
  • USER

console.log(cell.cellIndex, cell.parentElement.rowIndex);  // デバッグのためのログ出力
の実行結果がundefined undefinedとなりました。

ちなみにその直前のconsole.log(cell)の出力結果は
script.js:42 <div class=​"cell black" data-row=​"3" data-col=​"5">​flex::after​</div>​
となりました。

cellがdiv要素のため、cellIndexなどがundefinedになるということでしょうか?
  • ASSISTANT

はい、その通りです。`cellIndex``rowIndex`は、`<td>``<tr>`要素特有のプロパティであり、`<div>`要素には存在しません。

提供された`console.log(cell)`の結果を見ると、カスタムデータ属性`data-row``data-col`が使用されているのがわかります。これを利用して、`x`および`y`の値を取得することができます。

以下のように修正してください:

function willFlipStones(cell, dir) {
    let x = parseInt(cell.getAttribute('data-col')) + dir[0];
    let y = parseInt(cell.getAttribute('data-row')) + dir[1];
    
    console.log(x, y);  // デバッグのためのログ出力
    ...
}

上記の修正により、`x`および`y`の値が正しく取得できるはずです。
  • USER

合わせて、以下の処理のように、tr:nth-child()やtd:nth-childセレクタにアクセスしている処理を全て修正した方が良いのではないでしょうか?
const nextCell = document.querySelector(`tr:nth-child(${y + 1}) td:nth-child(${x + 1})`);
  • ASSISTANT

はい、その通りです。`<div>`要素を使っているのであれば、`tr:nth-child()``td:nth-child()`セレクタは適切ではありません。

代わりに、`data-row``data-col`のカスタムデータ属性を利用して、要素を選択することができます。

修正は以下のように行うことができます:

const nextCell = document.querySelector(`.cell[data-row="${y}"][data-col="${x}"]`);

このセレクタは、`data-row``data-col`の属性値が、指定した`y`および`x`と一致する`<div>`要素を選択します。それにより、正しいセルが取得できるはずです。

この変更を、コード内のすべての該当する部分に適用してください。

実装結果

  • 一部バグがあるものの概ね石をひっくり返すロジックは実装できました。

最終的な盤面画像1
最終的な盤面画像2
最終的な盤面画像3

所感

本ロジックの実装時、画像入力はほとんど使用していませんが、GPT-4Vの提案コードを実装した結果の画像(石をひっくり返すロジックの実装結果1)を一度だけ読み込ませました。
しかし、その画像付きプロンプトに対するGPTの返答のソースコードを実装した後、エラーが発生しました。
したがって、この画像入力が効果的だったかどうかは不明です。

手番と石数の表示処理の実装

チャット

  • USER

本ゲームの元画像とソースコードを提示します。
元画像と同じように手番と黒白の石数を表示させてください。
updateDisplayとplaceStoneを実装してください。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>オセロゲーム</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="board">
        <!-- JavaScriptで盤面を生成します -->
    </div>
    <script src="script.js"></script>
</body>
</html>


const boardSize = 8;
const board = document.querySelector('.board');
let currentPlayer = 'black';
for (let i = 0; i < boardSize; i++) {
    for (let j = 0; j < boardSize; j++) {
        const cell = document.createElement('div');
        cell.classList.add('cell');
        cell.dataset.row = i;
        cell.dataset.col = j;
        board.appendChild(cell);
    }
}
document.querySelector(`[data-row='${boardSize/2-1}'][data-col='${boardSize/2-1}']`).classList.add('white');
document.querySelector(`[data-row='${boardSize/2}'][data-col='${boardSize/2}']`).classList.add('white');
document.querySelector(`[data-row='${boardSize/2-1}'][data-col='${boardSize/2}']`).classList.add('black');
document.querySelector(`[data-row='${boardSize/2}'][data-col='${boardSize/2-1}']`).classList.add('black');
const cells = document.querySelectorAll('.cell');
cells.forEach(cell => {
    cell.addEventListener('click', function() {
        console.log(this);
        if (!this.classList.contains('black') && !this.classList.contains('white')) {
            this.classList.add(currentPlayer);
            if (isValidMove(this)) {
                flipStones(this);
                currentPlayer = currentPlayer === 'black' ? 'white' : 'black';
            } else {
                this.classList.remove(currentPlayer);
            }
        }
    });
});
function isValidMove(cell) {
    const directions = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]];
    for (let dir of directions) {
        if (willFlipStones(cell, dir)) {
            return true;
        }
    }
    return false;
}
function willFlipStones(cell, dir) {
    // let x = cell.cellIndex + dir[0];
    // let y = cell.parentElement.rowIndex + dir[1];
    let x = parseInt(cell.getAttribute('data-col')) + dir[0];
    let y = parseInt(cell.getAttribute('data-row')) + dir[1];
    if (outOfBounds(x, y)) return false;
    // const nextCell = document.querySelector(`tr:nth-child(${y + 1}) td:nth-child(${x + 1})`);
    const nextCell = document.querySelector(`.cell[data-row="${y}"][data-col="${x}"]`);
    if (!nextCell.classList.contains(oppositePlayer(currentPlayer))) return false;
    while (!outOfBounds(x, y)) {
        // const currentCell = document.querySelector(`tr:nth-child(${y + 1}) td:nth-child(${x + 1})`);
        const currentCell = document.querySelector(`.cell[data-row="${y}"][data-col="${x}"]`);
        if (currentCell.classList.contains(currentPlayer)) {
            return true;
        }
        x += dir[0];
        y += dir[1];
    }
    return false;
}
function outOfBounds(x, y) {
    return x < 0 || x >= 8 || y < 0 || y >= 8;
}
function flipStones(cell) {
    const directions = [
        [-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]
    ];
    for (let dir of directions) {
        // let x = cell.cellIndex + dir[0];
        // let y = cell.parentElement.rowIndex + dir[1];
        let x = parseInt(cell.getAttribute('data-col')) + dir[0];
        let y = parseInt(cell.getAttribute('data-row')) + dir[1];
        const stonesToFlip = [];
        while (!outOfBounds(x, y)) {
            // const currentCell = document.querySelector(`tr:nth-child(${y + 1}) td:nth-child(${x + 1})`);
            const currentCell = document.querySelector(`.cell[data-row="${y}"][data-col="${x}"]`);
            if (currentCell.classList.contains(currentPlayer)) {
                stonesToFlip.forEach(stone => stone.classList.replace(oppositePlayer(currentPlayer), currentPlayer));
                break;
            } else if (!currentCell.classList.contains(oppositePlayer(currentPlayer))) {
                break; }
            stonesToFlip.push(currentCell);
            x += dir[0];
            y += dir[1];
        }
    }
}
function oppositePlayer(player) {
    return player === 'black' ? 'white' : 'black';
}



body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f4f4f4;
}

.board {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    width: 321px; /* 1pxを追加してボーダーを考慮 */
    height: 321px; /* 1pxを追加してボーダーを考慮 */
    background-color: black;
}

.cell {
    box-sizing: border-box;
    width: 40px;
    height: 40px;
    background-color: green;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    position: relative; 
    border-right: 1px solid black; 
    border-bottom: 1px solid black; 
}

/* 最右列のボーダーを修正 */
.cell:nth-child(8n) {
    border-right: none; 
}

/* 最下列のボーダーを修正 */
.cell:nth-last-child(-n+8) {
    border-bottom: none; 
}

.cell.black::after, .cell.white::after {
    content: '';
    position: absolute;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%); 
}

.cell.black::after {
    background-color: black;
}

.cell.white::after {
    background-color: white;
}
  • ASSISTANT

了解しました。以下のように手番と黒白の石数を表示するように修正します。

1. HTMLに手番と石数の表示エリアを追加:
```html
<div class="board-info">
  <p>手番: <span id="currentPlayerDisplay">黒</span></p>
  <p>黒: <span id="blackScore">2</span></p>
  <p>白: <span id="whiteScore">2</span></p>
</div>

2. `updateDisplay`関数と`placeStone`関数をJavaScriptに追加:

function updateDisplay() {
  const blackScore = document.querySelectorAll('.black').length;
  const whiteScore = document.querySelectorAll('.white').length;

  document.getElementById('blackScore').textContent = blackScore;
  document.getElementById('whiteScore').textContent = whiteScore;
  document.getElementById('currentPlayerDisplay').textContent = currentPlayer === 'black' ? '黒' : '白';
}

function placeStone(cell) {
  if (!cell.classList.contains('black') && !cell.classList.contains('white')) {
    cell.classList.add(currentPlayer);
    if (isValidMove(cell)) {
      flipStones(cell);
      currentPlayer = currentPlayer === 'black' ? 'white' : 'black';
      updateDisplay();
    } else {
      cell.classList.remove(currentPlayer);
    }
  }
}

// 既存のイベントリスナーで`placeStone`関数を使用
cells.forEach(cell => {
  cell.addEventListener('click', function() {
    placeStone(this);
  });
});



3. CSSに手番と石数の表示エリアのスタイルを追加:

.board-info {
  margin-top: 15px;
  font-size: 18px;
}

これで、石が置かれたりひっくり返されるたびに手番と黒白の石数が更新されるようになります。
  • USER

手番と石数がほぼ正しく表示されるようになりました。
ただし、画像のように右側にこれらが表示されます。
元画像のように下側に表示させるようにしてください。
  • ASSISTANT

了解しました。手番と石数を下側に表示させるために以下の修正を行います。

1. HTMLの構造を変更し、盤面の下に情報を表示:

<div class="game-container">
    <div class="board">
        <!-- JavaScriptで盤面を生成します -->
    </div>
    <div class="board-info">
      <p>手番: <span id="currentPlayerDisplay">黒</span></p>
      <p>黒: <span id="blackScore">2</span></p>
      <p>白: <span id="whiteScore">2</span></p>
    </div>
</div>

2. CSSの変更:

.game-container {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.board-info {
    margin-top: 15px;
    font-size: 18px;
}

`.game-container`で`flex-direction: column;`を使用することで、子要素(盤面と情報)が縦に並ぶようになります。上記の修正により、手番と石数が盤面の下に正しく表示されるようになるはずです。
  • USER

画像のように、手番と石数が下に表示されるようになりましたが、表示が微妙に元画像と異なります。
具体的には、以下の点で異なります。この点を修正してください。

- テキストの垂直方向の間隔
    - 元画像
        - ほぼ0
    - 本画像
        - 元画像より大きい
- 石数表示
    - 元画像
        - 黒: 2, 白: 2
    - 本画像
        - 黒: 2\n 白: 2 
  • ASSISTANT

了解しました。指摘いただいた点を修正します。

1. テキストの垂直方向の間隔を修正:

`.board-info` のスタイルに以下を追加します:

.board-info p {
    margin: 0;
}

2. 石数の表示を1行で表示するように変更:

HTMLの以下の部分を修正します:

<p>黒: <span id="blackScore">2</span>, 白: <span id="whiteScore">2</span></p>

上記の修正により、元の画像と同様の表示になるはずです。
  • USER

画像は実装結果です。
まだ微妙に手番と石数のスタイルが元画像と異なります。
元画像と全く同じスタイルにしてください。
  • ASSISTANT

了解しました。以下の手順で、手番と石数のスタイルを元画像と全く同じに調整します。

1. CSSで`.board-info`のfont-sizeline-heightを調整することで、表示の大きさや行の高さを調整します。

2. `.board-info`のpaddingmarginを調整することで、間隔や位置を調整します。

具体的な修正点は以下の通りです:

CSSの追加・変更:

.board-info {
    font-size: 18px; /* フォントサイズの調整 */
    line-height: 1.2; /* 行の高さの調整 */
    margin-top: 10px; /* 上方向の余白の調整 */
}

.board-info p {
    margin: 0;
    padding: 0;
}

これで、手番と石数の表示が元画像と同じスタイルになるはずです。もし調整が必要な場合、上記の値を微調整してください。

実装結果

最終的な、テキスト表示処理の実装結果1
最終的な、テキスト表示処理の実装結果2
元画像
  • 表示のロジック

    • 後述のバグがありますが、手番と石数は概ね正しく表示されるようになりました。

  • テキストのデザイン

    • 元画像はテキストが左寄せですが、再現画像は中央寄せです。

    • テキストの垂直方向の間隔が本画像と微妙に異なります。

所感

テキストのデザインに関しても、GPT-4Vは類似のデザインであればサクッと再現できますが、同一のデザインを再現することは難しいようです。
同一デザインを再現させる場合は、同様にGPT-4Vに作らせたベースのデザインを人間が手動で加工するのが効率的だと思います。

バグ修正

チャット

  • USER

バグ画像1
バグ画像2
本ゲームには置けない場所に白石を置けるバグがあります。
画像1は白石を置く前、画像2は画像1の局面において置けない場所に白石を置いた直後の画像となります。 
このバグの原因を教えてください。
  • ASSISTANT

画像から推測すると、以下の点を考慮してバグの原因を調査するべきです。

1. ロジックの不具合: ゲームの基本的なルールに基づいて石が配置できる場所をチェックするロジックに不具合がある可能性があります。具体的には、隣接する石の色や自身の石が囲まれていないかをチェックする処理が不十分か誤っている可能性が考えられます。

2. イベントハンドラの不具合: ユーザーが石を配置しようとしたときのイベントハンドラ(クリックイベントなど)に問題があるかもしれません。正しいセルの座標が取得されていない、または誤った方法で座標が処理されている可能性があります。

3. ゲームの状態管理: ゲームの現在の状態(どのプレイヤーのターンか、各セルの状態など)を管理するデータ構造や変数に問題があるかもしれません。これにより、実際のゲームの状態と表示されている状態との間に不整合が生じることが考えられます。

4. レンダリングの問題: 正しく石を配置できるロジックが実装されているにも関わらず、画面上に正しくレンダリングされていない可能性があります。この場合、内部のゲームの状態は正しく、表示だけが誤っているということになります。

画像だけでは具体的なコードや実装の詳細を知ることはできませんが、上記の点を考慮してコードのレビューを行うことで、バグの原因を特定しやすくなるでしょう。
  • USER

本ゲームのソースコードです。
原因の特定をお願いします。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>オセロゲーム</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="game-container">
        <div class="board">
            <!-- JavaScriptで盤面を生成します -->
        </div>
        <div class="board-info">
            <p>手番: <span id="currentPlayerDisplay">黒</span></p>
            <p>黒: <span id="blackScore">2</span>, 白: <span id="whiteScore">2</span></p>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f4f4f4;
}

.board {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    width: 321px; /* 1pxを追加してボーダーを考慮 */
    height: 321px; /* 1pxを追加してボーダーを考慮 */
    background-color: black;
}


.cell {
    box-sizing: border-box;
    width: 40px;
    height: 40px;
    background-color: green;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    position: relative; 
    border-right: 1px solid black; 
    border-bottom: 1px solid black; 
}

/* 最右列のボーダーを修正 */
.cell:nth-child(8n) {
    border-right: none; 
}

/* 最下列のボーダーを修正 */
.cell:nth-last-child(-n+8) {
    border-bottom: none; 
}

.cell.black::after, .cell.white::after {
    content: '';
    position: absolute;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%); 
}

.cell.black::after {
    background-color: black;
}

.cell.white::after {
    background-color: white;
}

.game-container {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.board-info {
    font-size: 18px; /* フォントサイズの調整 */
    line-height: 1.2; /* 行の高さの調整 */
    margin-top: 10px; /* 上方向の余白の調整 */
}

.board-info p {
    margin: 0;
    padding: 0;
}


const boardSize = 8;
const board = document.querySelector('.board');
let currentPlayer = 'black';
for (let i = 0; i < boardSize; i++) {
    for (let j = 0; j < boardSize; j++) {
        const cell = document.createElement('div');
        cell.classList.add('cell');
        cell.dataset.row = i;
        cell.dataset.col = j;
        board.appendChild(cell);
    }
}
document.querySelector(`[data-row='${boardSize/2-1}'][data-col='${boardSize/2-1}']`).classList.add('white');
document.querySelector(`[data-row='${boardSize/2}'][data-col='${boardSize/2}']`).classList.add('white');
document.querySelector(`[data-row='${boardSize/2-1}'][data-col='${boardSize/2}']`).classList.add('black');
document.querySelector(`[data-row='${boardSize/2}'][data-col='${boardSize/2-1}']`).classList.add('black');
const cells = document.querySelectorAll('.cell');
cells.forEach(cell => {
    cell.addEventListener('click', function() {
        placeStone(this);
    });
});
function isValidMove(cell) {
    const directions = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]];
    for (let dir of directions) {
        if (willFlipStones(cell, dir)) {
            return true;
        }
    }
    return false;
}
function willFlipStones(cell, dir) {
    let x = parseInt(cell.getAttribute('data-col')) + dir[0];
    let y = parseInt(cell.getAttribute('data-row')) + dir[1];
    if (outOfBounds(x, y)) return false;
    const nextCell = document.querySelector(`.cell[data-row="${y}"][data-col="${x}"]`);
    if (!nextCell.classList.contains(oppositePlayer(currentPlayer))) return false;
    while (!outOfBounds(x, y)) {
        const currentCell = document.querySelector(`.cell[data-row="${y}"][data-col="${x}"]`);
        if (currentCell.classList.contains(currentPlayer)) {
            return true;
        }
        x += dir[0];
        y += dir[1];
    }
    return false;
}
function outOfBounds(x, y) {
    return x < 0 || x >= 8 || y < 0 || y >= 8;
}
function flipStones(cell) {
    const directions = [
        [-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]
    ];
    for (let dir of directions) {
        let x = parseInt(cell.getAttribute('data-col')) + dir[0];
        let y = parseInt(cell.getAttribute('data-row')) + dir[1];
        const stonesToFlip = [];
        while (!outOfBounds(x, y)) {
            const currentCell = document.querySelector(`.cell[data-row="${y}"][data-col="${x}"]`);
            if (currentCell.classList.contains(currentPlayer)) {
                stonesToFlip.forEach(stone => stone.classList.replace(oppositePlayer(currentPlayer), currentPlayer));
                break;
            } else if (!currentCell.classList.contains(oppositePlayer(currentPlayer))) {
                break; }
            stonesToFlip.push(currentCell);
            x += dir[0];
            y += dir[1];
        }
    }
}
function oppositePlayer(player) {
    return player === 'black' ? 'white' : 'black';
}
function updateDisplay() {
    const blackScore = document.querySelectorAll('.black').length;
    const whiteScore = document.querySelectorAll('.white').length;
    document.getElementById('blackScore').textContent = blackScore;
    document.getElementById('whiteScore').textContent = whiteScore;
    document.getElementById('currentPlayerDisplay').textContent = currentPlayer === 'black' ? '黒' : '白';
}
function placeStone(cell) {
    if (!cell.classList.contains('black') && !cell.classList.contains('white')) {
        cell.classList.add(currentPlayer);
        if (isValidMove(cell)) {
            flipStones(cell);
            currentPlayer = currentPlayer === 'black' ? 'white' : 'black';
            updateDisplay();
        } else {
            cell.classList.remove(currentPlayer);
        }
    }
}
  • ASSISTANT

バグの原因は以下の部分にあると思われます:

function placeStone(cell) {
    if (!cell.classList.contains('black') && !cell.classList.contains('white')) {
        cell.classList.add(currentPlayer);
        if (isValidMove(cell)) {
            flipStones(cell);
            currentPlayer = currentPlayer === 'black' ? 'white' : 'black';
            updateDisplay();
        } else {
            cell.classList.remove(currentPlayer);
        }
    }
}

この`placeStone`関数では、まず指定されたセルに石を置く操作が始まると、現在のプレイヤーの石をそのセルに配置します。その後、`isValidMove`関数でその動作が有効かどうかをチェックします。もし有効でない場合、`cell.classList.remove(currentPlayer);`でその石を削除します。

しかしこのロジックでは、まず無条件に石を配置してから、その配置が有効かどうかを確認するという流れになっています。これが不適切な配置によるバグの原因と考えられます。

このバグを修正するためには、`isValidMove`関数で動きが有効であることを確認した後にのみ石を配置するように変更する必要があります。

以下のように修正すると良いでしょう:

function placeStone(cell) {
    if (!cell.classList.contains('black') && !cell.classList.contains('white')) {
        if (isValidMove(cell)) {
            cell.classList.add(currentPlayer);
            flipStones(cell);
            currentPlayer = currentPlayer === 'black' ? 'white' : 'black';
            updateDisplay();
        }
    }
}

この変更により、`isValidMove``true`を返す場合のみ石を配置するようになり、不適切な配置が防げるはずです。
  • USER

改善しませんでした。画像1の局面において左上に石を置けてしまい、その後画像2の局面になりました。

石を置いた後、isValidMove()でその動作が有効かどうかをチェックして、無効な場合削除するのであれば、あまり問題ない気がします。
isValidMove()のロジックに問題がある気がします。
  • ASSISTANT

了解しました。問題は`isValidMove`関数のロジックにありそうですね。

`isValidMove`関数は、指定されたセルに石を置くことができるかどうかを確認する関数です。この関数では、指定されたセルから8方向を調べ、その方向に現在のプレイヤーとは異なる色の石が連続しており、その後に現在のプレイヤーと同じ色の石があるかどうかを確認しています。

問題が生じているのは、この関数のロジックが正しく動作していないことに起因している可能性が高いです。

まず、`isValidMove`関数の中の`willFlipStones`関数を確認しましょう。

`willFlipStones`関数では、指定されたセルから指定された方向に進んで、石をひっくり返せるかどうかを確認しています。しかし、この関数の中で、現在のプレイヤーと同じ色の石を見つけたときに、その位置までのすべての石が現在のプレイヤーとは異なる色であることを確認していません。

このことから、`willFlipStones`関数のロジックを以下のように修正する必要があるかもしれません:


function willFlipStones(cell, dir) {
    let x = parseInt(cell.getAttribute('data-col')) + dir[0];
    let y = parseInt(cell.getAttribute('data-row')) + dir[1];
    let hasOppositeColor = false;
    while (!outOfBounds(x, y)) {
        const currentCell = document.querySelector(`.cell[data-row="${y}"][data-col="${x}"]`);
        if (currentCell.classList.contains(currentPlayer)) {
            return hasOppositeColor;
        } else if (currentCell.classList.contains(oppositePlayer(currentPlayer))) {
            hasOppositeColor = true;
        } else {
            break;
        }
        x += dir[0];
        y += dir[1];
    }
    return false;
}

この修正により、指定されたセルから指定された方向に進むときに、現在のプレイヤーとは異なる色の石があること、そしてその後に現在のプレイヤーと同じ色の石があることを確認して、その間の石をひっくり返せるかどうかを判断することができるようになります。

この変更を行うことで、ゲームの動作が正しくなるかどうかを確認してみてください。
  • USER

改善したようですが、改善した理由がよくわかりませんでした。

「`willFlipStones`関数では、指定されたセルから指定された方向に進んで、石をひっくり返せるかどうかを確認しています。しかし、この関数の中で、現在のプレイヤーと同じ色の石を見つけたときに、その位置までのすべての石が現在のプレイヤーとは異なる色であることを確認していません。」
とのことですが、この点について疑問があります。

「現在のプレイヤーと同じ色の石を見つけたときに、その位置までのすべての石が現在のプレイヤーとは異なる色であることを確認しなければおかしくなる場合」
とはどういう場合でしょうか?
従来の場合でも、その位置までの色が現在のプレイヤーと異なる色であることは保証されていると思います。
  • ASSISTANT

申し訳ございません。再度、`willFlipStones`関数のロジックを見直して、誤りを正確に理解し、説明いたします。

まず、従来の`willFlipStones`関数のロジックを確認すると、指定された方向に進みながら次の2つの条件を確認しています:

1. 現在のプレイヤーと異なる色の石が連続していること
2. 現在のプレイヤーと同じ色の石が存在すること

このロジックの問題点は、1の条件に合致するが2の条件に合致しない場合でも、`true`を返してしまう点にあります。つまり、異なる色の石が連続しているだけで、最終的に現在のプレイヤーと同じ色の石が存在しない場合でも、石をひっくり返せると誤判定してしまいます。

修正したロジックでは、`hasOppositeColor`変数を使用して、現在のプレイヤーと異なる色の石が1つでも存在するかどうかを確認しています。その後、現在のプレイヤーと同じ色の石が見つかったとき、`hasOppositeColor``true`であれば、石をひっくり返せると判断して`true`を返します。

この修正により、指定された方向に進んだとき、現在のプレイヤーと同じ色の石が見つかる前に、異なる色の石が1つも存在しない場合は、`false`を返すようになり、正確な動作が保証されるようになります。

ご指摘いただき、ありがとうございます。説明が不足しており、混乱を招く結果となってしまい、申し訳ございませんでした。
  • USER

それは違う気がします。
従来の場合trueを返すのは以下の箇所のみです。

        if (currentCell.classList.contains(currentPlayer)) {
            return true;
        }

つまり、2の現在のプレイヤーと同じ色の石が存在した場合のみであり、「2の条件に合致しない場合でも、`true`を返してしまう」ということはないと思います。
  • ASSISTANT

大変申し訳ございません、おっしゃるとおりです。

お示しいただいた`willFlipStones`のコードを再確認いたしますと、現在のプレイヤーと同じ色の石を見つけたときに`true`を返すのは、その前に現在のプレイヤーとは異なる色の石が連続していた場合のみであり、おっしゃるとおり、異なる色の石が連続していない場合には`true`を返すことはありません。

誤った情報をお伝えしてしまい、ご不便をおかけしてしまい申し訳ございません。

では、再度ソースコードを精査し、問題を特定します。

`placeStone`関数で、`isValidMove`の前に石を置いてから、その後で有効な手かどうかを確認していることが原因のようです。

function placeStone(cell) {
    if (!cell.classList.contains('black') && !cell.classList.contains('white')) {
        cell.classList.add(currentPlayer); // ここで石を置く
        if (isValidMove(cell)) { // ここで有効かどうかを確認
            flipStones(cell);
            currentPlayer = currentPlayer === 'black' ? 'white' : 'black';
            updateDisplay();
        } else {
            cell.classList.remove(currentPlayer); // 有効でなければ取り消す
        }
    }
}

`isValidMove`の中で`willFlipStones`を使用して、石をひっくり返せるかどうかを確認していますが、この時点で`cell`は既に現在のプレイヤーの色の石が置かれているため、このチェックが正確に動作しない可能性があります。

したがって、以下のように`isValidMove`の前に石を置くのではなく、まずは有効な手かどうかを確認してから石を置くように修正することで問題を解消できるかと思います。


function placeStone(cell) {
    if (!cell.classList.contains('black') && !cell.classList.contains('white')) {
        if (isValidMove(cell)) {
            cell.classList.add(currentPlayer); // 有効であれば石を置く
            flipStones(cell);
            currentPlayer = currentPlayer === 'black' ? 'white' : 'black';
            updateDisplay();
        }
    }
}

この変更により、石を置く前に手が有効であるかのチェックを行い、その後で石を置くようになり、正確に動作すると考えられます。
  • USER

原因はおそらく、`willFlipStones`関数で、指定されたセルから指定された方向に進んで、石をひっくり返せるかどうかを確認時、調査中のセルが、石が置かれていない色のついていないセルである場合の処理が適切に行われていない点にある気がします。

従来は石が置かれていないセルは、反対色の石と判断されますが、この空白セルを見つけた時点でfalseを返すようにすべきだと思います。
  • ASSISTANT

おっしゃる通り、`willFlipStones`関数内で空のセル(石が置かれていないセル)に遭遇した場合に、すぐに`false`を返すように変更することで問題が解消される可能性があります。

`willFlipStones`関数内で、セルの色のチェックを行う部分に以下のようなコードを追加すれば、この問題を修正できるかと思います。

const currentCell = document.querySelector(`.cell[data-row="${y}"][data-col="${x}"]`);
if (!currentCell.classList.contains('black') && !currentCell.classList.contains('white')) {
    return false; // セルが空の場合、直ちにfalseを返す
}

この変更を行うことで、指定された方向に進む際に空のセルを検出したらすぐに`false`を返し、その方向での石の反転が行えないことを示すようになります。これによって、石をひっくり返せるかどうかの判断が正確に行われると考えられます。
  • USER

改善しました。ありがとうございました!
  • ASSISTANT

良かったです!ご不明点や追加の質問などがあれば、お気軽にご連絡ください。
ゲームの開発や改善の過程でのお手伝いができて嬉しいです。引き続き楽しんでプログラミングを進めてくださいね!

修正結果

  • 置けない場所に白石を置けるバグは修正され、本ゲームが完成しました。

完成ゲーム局面1
完成ゲーム局面2
完成ゲーム局面3

バグの原因と改善方法

  • 本バグは、本チャット冒頭の「バグ画像1」、「バグ画像2」のように、本来置けない場所に石が置けてしまう場合があるバグです。

  • GPT-4Vが指摘した原因は、「placeStone(cell)関数内で、無条件に現在のプレイヤーの色の石を配置する処理が実行されていること」というものでした。

    • placeStone(cell)関数は、指定したcellにisValidMove(cell)がtrueの時のみ、現在のプレイヤーの石を置きます。

      • isValidMove(cell)は、cellに現在のプレイヤーの石を置くことができるかどうかを判定する関数です。

      • 無条件に石を配置した後、isValidMove(cell)がfalseであれば、その石を削除する流れになっています。

  • しかし、実際の原因は、「isValidMove()関数内のwillFlipStones(cell, dir)関数において、探索セル内に空白セルがあった場合にfalseを返す処理がなかったこと」でした。

    • willFlipStones(cell, dir)は、指定されたセルcellから指定された方向dirに向かってセルを探索し、cellに問題なく石を置けるかどうかを確認する関数です。石を置ける場合はtrueを、置けない場合はfalseを返します。

      • 従来は、dir上の最も遠い端の石の色が現在のプレイヤーの色であり、かつcellからその端までの間に自分のプレイヤーの石が置かれていない場合にのみtrueを返す関数でした。

        • 従来は、石が置かれていない空白セルも、プレイヤーの石が置かれていないセルと判定されtrueを返していました。

        • そのため、例えば下の図のようにF6が空白セルの場合、H4に石が置けるバグが発生することになります。

        • そこで、空白セルが存在する場合はfalseを返すように修正することで、このバグは改善しました。

バグの原因の図解

所感

  • 画像入力により、本バグ発生時の状況(局面)を詳細に伝える手間が省けたため、生産性が向上した可能性はあるかもしれません。

    • 仮にバグ発生時の局面を言語のみで伝達しようとすると、膨大な労力が必要になると思います。

  • しかし、GPT-4Vが提示したバグの対処を実装しても、バグは改善されませんでした。

  • そのため、GPT-4Vがこの局面をどの程度正確に認識できており、また画像入力が生産性をどれほど向上させたかは不明です。

  • 画像入力とは関連性が低いですが、本バグ修正を完了させるために、(画像入力を行わずに)GPT-4Vとペアプロミングを行い、以下の所感を得ました。

    • 上で触れた通り、GPT-4Vは本バグの原因を正しく指摘することができませんでした。

    • そのため、本著者がコードを精査することで、新の原因を発見しました。

    • 以前投稿したこちらの記事で、GPT-4は単純なバグであっても、その原因を発見できない場合があると結論づけました。

    • そのため、バグ修正を行う場合、今回のように、バグの原因調査を人間が手動で行い、その原因をGPTに提示して、修正コードをGPTに生成させるのが効率的かもしれません。

    • そのためにもコードを人間がきちんと理解するスキルが重要になると思います。

まとめ


本記事ではGPT-4Vのマルチモーダル機能(画像認識)のゲーム開発への応用能力の知識共有を目的に、オセロゲームを題材に、画像認識機能を利用しながらゲーム開発を行いました。

まず、冒頭で既存のオセロゲームのばデザインを読み込ませて再現させました。その結果、類似のデザインがサクッと再現され、生産性が向上したと感じました。
一方、1pxもズレもないようなデザインを再現させることにも挑戦しましたが、結局実装できませんでした。
完璧なゲームのデザインを再現させたい場合は、類似のデザインをGPT-4Vにサクッと再現させ、それを人間が修正するのが効率的だと感じました。

他にも、バグの修正依頼を出す際に、ハグ発生時の画像を読み込ませ、原因とその改善策を生成させることなども試しました。
バグの詳細な状況を言語で伝達する手間が省けはしましたが、GPT-4Vが提示した改善案のコードを実装しても、バグが改善されなかったため、生産性が向上したかは微妙なところです。

これらの結果から、現状は少なくとも類似のゲームデザインを再現させる場合には、画像入力機能を利用することで、生産性が向上する可能性があると結論付けることにします。


この記事が気に入ったらサポートをしてみませんか?