きっかけ
いまのBingChatはGPT-4を搭載しているという記事( https://qiita.com/takao-takass/items/16a7052a4a0e857b7c90 )を読んで、早速使ってみた話。
色々と記録を残しそびれたので、まずは結果だけを載せます。
コード
<!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>
.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;
}
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でまとめようねとかそういうレベルじゃないんだっていう驚き。もうこれだけで筆者を完全に越えました。
まだまだ、改善の余地はありそうですが、ひとまずこの辺で。