GPT-4を搭載したBingチャットでオセロゲーム書かせたらサクッと出来て感動した話

# きっかけ
いまのBingChatはGPT-4を搭載しているという記事（ https://qiita.com/takao-takass/items/16a7052a4a0e857b7c90 ）を読んで、早速使ってみた話。
色々と記録を残しそびれたので、まずは結果だけを載せます。

# コード
~~~othello.html
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
    <div id="player" style="font-size: 24px; margin-bottom: 10px;"></div>
    <div id="board" class="board"></div>

    <script type="text/javascript" src="src/othello.js"></script>
    <noscript>JavaScriptが利用できません．</noscript>
</body>
</html>
~~~
~~~css/style.css
.board {
    display: grid;
    grid-template-columns: repeat(8, 50px);
    grid-template-rows: repeat(8, 50px);
    gap: 1px;
}
.cell {
    width: 50px;
    height: 50px;
    background-color: darkgreen;
    border: 1px solid black;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 2em;
}
.stone {
    width: 80%;
    height: 80%;
    border-radius: 50%;
}
.black {
    background-color: black;
}
.white {
    background-color: white;
}
~~~
~~~othello.js
class OthelloGame {
  constructor() {
    this.board = Array(8).fill().map(() => Array(8).fill(0));
    this.board[3][3] = this.board[4][4] = 1;
    this.board[3][4] = this.board[4][3] = -1;
    this.currentPlayer = 1;
    this.directions = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];
  }

  traverseBoard(x, y, color, callback) {
    for(let [dx, dy] of this.directions){
      let nx = x + dx;
      let ny = y + dy;
      let count = 0;
      while(nx >= 0 && nx < 8 && ny >= 0 && ny < 8 && this.board[nx][ny] == -color){
        nx += dx;
        ny += dy;
        count++;
      }
      if(count > 0 && nx >= 0 && nx < 8 && ny >= 0 && ny < 8 && this.board[nx][ny] == color){
        callback(x, y, dx, dy, count);
      }
    }
  }

  canPlaceStone(x, y, color) {
    if(this.board[x][y] != 0) return false;
    let canPlace = false;
    this.traverseBoard(x, y, color, () => canPlace = true);
    return canPlace;
  }

  doPlaceStone(x, y, color) {
    this.traverseBoard(x, y, color, (x, y, dx, dy, count) => {
      let nx = x;
      let ny = y;
      for(let j = 0; j <= count; j++){
        this.board[nx][ny] = color;
        nx += dx;
        ny += dy;
      }
    });
  }

  hasLegalMove(color) {
    return this.board.some((row,x) => row.some((cell,y) => this.canPlaceStone(x,y,color)));
  }

  calculateScore() {
    let black = Array.prototype.concat(...this.board).filter(x => x === 1).length;
    let white = Array.prototype.concat(...this.board).filter(x => x === -1).length;
    return {black: black, white: white};
  }

}

class OthelloGameUI {
  constructor(game) {
    this.game = game;
    this.boardElement = document.getElementById('board');
    this.playerElement = document.getElementById('player');

    for(let i = 0; i < 8; i++){
      for(let j = 0; j < 8; j++){
        let cellElement = document.createElement('div');
        cellElement.className = 'cell';
        cellElement.addEventListener('click', () => {
          if(this.game.canPlaceStone(i, j, this.game.currentPlayer)) {
            this.game.doPlaceStone(i, j, this.game.currentPlayer);
            this.game.currentPlayer *= -1;
            if(!this.game.hasLegalMove(this.game.currentPlayer)) {
              this.game.currentPlayer *= -1;
              if(!this.game.hasLegalMove(this.game.currentPlayer)) {
                let score = this.game.calculateScore();
                let winner = score.black > score.white ? '黒' : '白';
                alert('ゲーム終了\n' + '黒: ' + score.black + ', 白: ' + score.white + '\n勝者: ' + winner);
              }
            }
            if(this.game.currentPlayer == -1) { // AI（白）の手番
              if(!this.game.hasLegalMove(this.game.currentPlayer)) {
                this.game.currentPlayer *= -1;
              }
              this.drawBoard();
            } else {
              if(!this.game.hasLegalMove(this.game.currentPlayer)) {
                this.game.currentPlayer *= -1;
              }
              this.drawBoard();
            }
            this.drawBoard();
          }
        });
        this.boardElement.appendChild(cellElement);
      }
    }

    this.drawBoard();
  }

  drawBoard() {
    for(let i = 0; i < 8; i++){
      for(let j = 0; j < 8; j++){
        let cellElement = this.boardElement.children[i * 8 + j];
        cellElement.innerHTML = '';
        if(this.game.board[i][j] != 0){
          let stoneElement = document.createElement('div');
          stoneElement.classList.add('stone');
          if(this.game.board[i][j] == 1){
            stoneElement.classList.add('black');
          } else if(this.game.board[i][j] == -1){
            stoneElement.classList.add('white');
          }
          cellElement.appendChild(stoneElement);
        }
      }
    }

    this.playerElement.textContent = '現在のプレイヤー: ' + (this.game.currentPlayer == 1 ? '黒' : '白');
  }
}

let game = new OthelloGame();
let ui = new OthelloGameUI(game);
ui.drawBoard();
~~~

# どう出したか
正直に言えば、一発でここまで出たわけではありません。
最初は「簡単ではございますが……」みたいに遠慮がちに、全く動かないものを出してきたこともありました。
そして、これはBing Chatの仕様なのでしょうが、1000文字までしか入力できませんし、5回までしか返事がもらえません。それゆえの工夫も必要でした。

## ファイルを分けさせた。
「htmlでオセロゲームを作りたい。htmlとcssとjavascriptで分けて、コードを書いて」
と最初に指示をして、書かせました。これだけで大分いい感じのものが出てきました。動きませんでしたが。

## 理想通り動くまで要求を繰り返す。
分離させたおかげで、jsのコードも全文載せられました（最初は）。
そこで、実際に動かしてみたときの不具合を、コードと共に突き付けて、
「このままだとこれこれこういうエラーがある。
　どこをどのように書き換えればいいか、正しいコードで返して」
とコードを必ず要求しました。コードを要求しないと

> このエラーの原因は次のようなことが考えられます
> 1.　～～～
> 2.　～～～

と、説明だけして、結局素人には何も分からないことになりました。どこを、どのように、コードを書き換えるのか、を必ず要求しました。

コードが長くなると、1回の1000文字制限を超えるようになりますが、途中で切り、その続きを次のチャットに入れて、要求を添えれば、きちんと反応してくれました。これはありがたかった。

## 最後に、リファクタリング
継ぎはぎで動くようにしただけのオセロゲームですから、どうせ、綺麗なコードになっているわけがないのは火を見るより明らかです。

Bing Chatにjsのコードをぶん投げて、最後に
「これをリファクタリングして」
の一言でここまでになりました。

最初はクラスなんて作ってなかったのに、OthelloGameクラスとOthelloGameUIクラスに分けて「こちらの方が保守もうんたらかんたら」と言っていたのにはマジでビックリしました。
同じものがあるからfunctionでまとめようねとかそういうレベルじゃないんだっていう驚き。もうこれだけで筆者を完全に越えました。

まだまだ、改善の余地はありそうですが、ひとまずこの辺で。