はじめに
皆さん、こんにちは!GxPの肥後です。
昔に作成したJava製オセロゲームを使ってAI開発してみようと思い立ち、最近話題のGemini CLIを使って、AIによるコードレビューやリファクタリングを導入してみることにしました。
この記事は、その過程で実際に実行したコマンド、遭遇したエラー(ハマりポイント)、そしてそれをどう乗り越えてコード品質を向上させたかを記載していきます。
- プロジェクト: Java製オセロゲーム 🕹️(最後にリンク記載しています)
- 目標: Gemini CLIを導入し、AIの力で開発ワークフローを効率化
- 開発環境: Alpine Linux v3.20 (Dev Container)
AI導入をするうえでの前提課題:まずは既存コードを分析
何はともあれ、かなり昔に作成したオセロゲームなのでまずは現状把握から。
既存のコードを読んでみると、いくつかの課題が見えてきました。
アーキテクチャの問題: Main.javaにあるべきボード配列の実体がなく、他クラスから参照エラーになる状態でした😅
ゲームになっていない: 石をひっくり返すロジックやゲーム終了条件がなく、ただ石を置けるだけ…
入力がお粗末: プレイヤーの入力値検証が甘く、エラーハンドリングも不十分でした。
これは、なかなか手強そうで、昔の私は途中で放り投げたようです。
第1章:人力でのロジック実装
AIに頼る前に、まずはゲームとして成立させるのが先決です。
早速、機能改善用のブランチを作成します。
# 機能改善用のブランチを作成してチェックアウト
git checkout -b feature/improved-othello-game
このブランチで、黙々とオセロの基本ロジックを実装しました。
Game.javaを新設: ゲーム進行、手番管理、バリデーションなど、頭脳となる部分をここに集約。
石の裏返しロジック: 8方向をチェックして、挟んだ石をひっくり返す処理を実装。
入力チェック強化: CheckInputValue.javaの正規表現を修正し、1~8の入力以外は弾くようにしました。
一通り実装が完了したら、変更内容をコミットします。
# 変更をステージング
git add .
# Conventional Commits規約に沿ってコミット
git commit -m "feat: 完全なオセロゲームロジックの実装
- ゲームクラスの追加
- 適切なオセロルールの実装
- 入力検証の改善
- ゲーム終了条件の実装
- コードの構造化"
これでようやく、人間同士で遊べるオセロが完成しました!🎉
第2章:AIアシスタント、Gemini CLI登場!…のはずが?
いよいよ本日の主役、Gemini CLIの出番です。
「このMain.javaをイケてる感じにリファクタリングしてよ!」と、以下のコマンドを叩きました。
cat src/main/java/com/example/Main.java | gemini "以下のJavaコードを、Java 17のStream APIやOptionalを使うなど、よりモダンで効率的な書き方にリファクタリングしてください。"
ハマりポイント①
まず最初に私を迎えてくれたのが、この無慈悲なエラーメッセージ。
真っ先にやるべきことを忘れていたので当然のごとく出ました。
# エラー: API key not valid
[API Error: {"error":{"code":400,"message":"API key not valid. Please pass a valid API key."}}]
Google AI Studioで取得したAPIキーを、以下のコマンドで環境変数に設定しました。
# 取得したAPIキーを設定 (your-api-key の部分を実際のキーに置き換えてください)
export GOOGLE_API_KEY="your-api-key"
ハマりポイント②:謎の "Unknown argument"
気を取り直して再度コマンドを実行!…すると、またもやエラー。
(エラー文章が長すぎて割愛)
解決策はgemini -r
を使うことでした。
-r
は簡単に言えばこのファイルの内容を読み込んだ上で、私の質問に答えてとGeminiにお願いするための機能だと思ってください。
NGコマンド
cat src/main/java/com/example/Main.java | gemini "以下のJavaコードを、Java 17のStream APIやOptionalを使うなど、よりモダンで効率的な書き方にリファクタリングしてください。"
OKコマンド
# -r フラグを使い、標準入力からプロンプトを組み立てるように変更
cat src/main/java/com/example/Main.java | gemini -r "このJavaコードをリファクタリングしてください"
第3章:AIとの協業、そして得られた成果
設定との格闘は続きましたが、試行錯誤の末、ついにGemini CLIが期待通りに動いてくれるようになりました!そして、その実力は想像以上でした。
✨ 成果①:Javaコードがモダンに生まれ変わった!
再度リファクタリングコマンドを実行したところ、今度は成功!Optionalを活用してネストを劇的に減らした、非常に洗練されたコードが返ってきました。
ここからプロンプトを英語に変更しています。
一般的に、コード生成やリファクタリングのような技術的な指示は、英語で与える方がAIが意図を正確に理解し、より質の高い結果を返す傾向があるためです。
リファクタリングコマンドを再実行
cat src/main/java/com/example/Main.java | gemini -r "Please refactor the following Java code to be more modern and efficient, for example by using Java 17 features like the Stream API and Optional. Also, add appropriate Javadoc comments. Provide only the refactored code in a markdown code block."
リファクタリング前のMain.java(Before)
package com.example;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
System.out.println("オセロ開始");
Game game = new Game();
ShowBoard showBoard = new ShowBoard();
Scanner scanner = new Scanner(System.in);
// ゲームループ
while (!game.isGameOver()) {
// ボード表示
showBoard.display(game.getBoard());
System.out.println("現在のプレイヤー: " + game.getCurrentPlayerSymbol());
// 横の入力
System.out.println("横の番号から数字で入力してください (1-8):");
String xInput = scanner.nextLine();
// 入力チェック
if (!CheckInputValue.isValid(xInput)) {
System.out.println("無効な入力です。1-8の数字を入力してください。");
continue;
}
int x = CheckInputValue.parseInput(xInput);
// 縦の入力
System.out.println("縦の番号から数字で入力してください (1-8):");
String yInput = scanner.nextLine();
// 入力チェック
if (!CheckInputValue.isValid(yInput)) {
System.out.println("無効な入力です。1-8の数字を入力してください。");
continue;
}
int y = CheckInputValue.parseInput(yInput);
// 石を置く
boolean success = game.placePiece(x, y);
if (!success) {
System.out.println("そこには置けません。もう一度入力してください。");
}
}
// ゲーム終了
showBoard.display(game.getBoard());
System.out.println("ゲーム終了");
// 最終スコア計算
int[][] board = game.getBoard();
int blackCount = 0;
int whiteCount = 0;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (board[i][j] == 1) {
blackCount++;
} else if (board[i][j] == 2) {
whiteCount++;
}
}
}
System.out.println("最終スコア - 黒: " + blackCount + ", 白: " + whiteCount);
if (blackCount > whiteCount) {
System.out.println("黒の勝利!");
} else if (whiteCount > blackCount) {
System.out.println("白の勝利!");
} else {
System.out.println("引き分け!");
}
scanner.close();
}
}
リファクタリング後のMain.java(After)
package com.example;
import java.util.Optional;
import java.util.Scanner;
import java.util.stream.IntStream;
/**
* Main class for the Othello game application.
* Manages the game loop and user interactions.
*/
public class Main {
private static final Scanner SCANNER = new Scanner(System.in);
private static final String GAME_START_MESSAGE = "オセロ開始";
private static final String GAME_OVER_MESSAGE = "ゲーム終了";
/**
* Entry point of the application.
* @param args command line arguments (not used)
*/
public static void main(String[] args) {
System.out.println(GAME_START_MESSAGE);
Game game = new Game();
ShowBoard showBoard = new ShowBoard();
while (!game.isGameOver()) {
showBoard.display(game.getBoard());
System.out.println("現在のプレイヤー: " + game.getCurrentPlayerSymbol());
getValidatedInput("横の番号")
.flatMap(x -> getValidatedInput("縦の番号")
.map(y -> new int[]{x, y}))
.ifPresentOrElse(
coords -> {
if (!game.placePiece(coords[0], coords[1])) {
System.out.println("そこには置けません。もう一度入力してください。");
}
},
() -> System.out.println("無効な入力です。")
);
}
showBoard.display(game.getBoard());
System.out.println(GAME_OVER_MESSAGE);
displayFinalScore(game);
}
/**
* Gets validated input from the user.
* @param prompt the prompt to display
* @return Optional containing the validated input (0-indexed), or empty if invalid
*/
private static Optional<Integer> getValidatedInput(String prompt) {
System.out.println(prompt + "から数字で入力してください (1-8):");
return Optional.ofNullable(SCANNER.nextLine())
.filter(CheckInputValue::isValid)
.map(CheckInputValue::parseInput);
}
/**
* Displays the final score of the game.
* @param game the game instance
*/
private static void displayFinalScore(Game game) {
int[][] board = game.getBoard();
long blackCount = IntStream.range(0, 8)
.flatMap(i -> IntStream.range(0, 8)
.map(j -> board[i][j]))
.filter(cell -> cell == 1)
.count();
long whiteCount = IntStream.range(0, 8)
.flatMap(i -> IntStream.range(0, 8)
.map(j -> board[i][j]))
.filter(cell -> cell == 2)
.count();
System.out.printf("最終スコア - 黒: %d, 白: %d%n", blackCount, whiteCount);
System.out.println(blackCount > whiteCount ? "黒の勝利!" :
whiteCount > blackCount ? "白の勝利!" : "引き分け!");
}
}
リファクタリングのポイント
1. 定数の抽出
- マジックストリングを定数として定義
- 保守性の向上
2. Optionalの活用
- null安全な入力処理
- ネストの深さを大幅に削減
-
flatMap
とmap
の連鎖で優雅な処理フロー
3. Stream APIの活用
- 最終スコアの計算を関数型スタイルで実装
- forループの代わりにIntStreamを使用
- 可読性と表現力の向上
4. メソッドの抽出
- 入力検証ロジックを別メソッドに
- スコア表示ロジックを別メソッドに
- 単一責任の原則に従った設計
5. Javadocコメントの追加
- クラスとメソッドに適切なドキュメント
- IDEでの開発体験の向上
✨ 成果②:README.mdが一瞬で完成!
面倒で後回しにしがちなREADMEの作成も、以下のコマンド一発でGeminiにお任せ。
コードを基にREADME.mdを生成
cat src/main/java/com/example/Main.java | gemini -r "Based on the following Java code, generate a README.md file for this project. Include the following sections: Project Title, Description, Build and Run (Maven/Gradle), and Usage. Provide the output in markdown format." > README.md
コードからプロジェクトの概要やビルド方法を的確にまとめたドキュメントが自動で生成されました。これは本当に便利!
最終章:胸を張ってPull Request
ここまでの改善をまとめて、いよいよPull Requestを作成します。
PRの説明文も、もちろんGeminiに下書きしてもらいます。
# mainブランチとの差分を基にPR説明文を生成
git diff main | gemini "Act as a senior software engineer. Based on the following code changes, write a clear and concise pull request description in Japanese. Include a title, a summary of the changes, and the reason for the change. Provide only the description content in markdown format."
生成された内容をGitHubのPR作成画面に貼り付け、最終的なPRが完成しました。
最終的なPull Requestの内容
オセロのゲームロジック実装とリファクタリング
概要
これまで石を置くだけの機能しかありませんでしたが、本PRにてオセロの主要なゲームロジックを実装し、実際にゲームがプレイできるようにしました。
また、ゲームロジックをGameクラスに分離することで、Mainクラスの責務をユーザー入出力の管理に限定し、コードのモジュール性を高め、今後の保守や機能追加を容易にすることを目的としています。
まとめ:今回の開発で学んだこと
今回の挑戦を通じて、多くの学びがありました。
AIツール導入のリアル: Gemini CLIは非常に強力ですが、環境構築やAPI設定でハマる可能性は十分にあります。ドキュメントをしっかり読み込むことの重要性を再認識しました。
AIとの付き合い方: AIは魔法の杖ではありません。まずは人間が基本設計やロジックをしっかり組み、その上で「リファクタリング」や「ドキュメント生成」といった定型的な作業を任せるのが、現時点では最適な協業スタイルだと感じました。
段階的な改善の重要性: 一度にすべてを変えようとせず、「まずは動くものを作る」→「AIの力も借りて洗練させる」というステップを踏んだことで、着実にプロジェクトを前進させることができました。
まだGemini CLIのポテンシャルをすべて引き出せているわけではありませんが、AIを開発の相棒にする面白さと可能性を大いに感じた挑戦でした。
次はJUnitでのテスト実装や、GitHub ActionsでのCI/CDパイプライン構築にも挑戦してみたいと思います!
最後まで読んでいただき、ありがとうございました!
使用したDockerfileはこちら
FROM python:3.12.5-alpine3.20
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir google-generativeai
RUN apk add --no-cache nodejs npm git && \
npm install -g @google/gemini-cli
RUN addgroup -S app && \
adduser -S -G app -h /home/app -s /bin/sh app && \
chown -R app:app /app
USER app
CMD [ "sh" ]
参考
gemini-cli - インストール手順やコマンドの詳細はこちら
Google AI Studio - APIキーの取得はこちらから
Conventional Commits - コミットメッセージの規約