Amazon Q Developer CLIで妙なローカルルールのリバーシゲームを作る
abstract
Amazon Q Developer CLIを利用してゲームを作成した。
ゲームの内容はリバーシの改造版。
AWSのTシャツをもらう。
AWS公式のキャンペーン記事は以下。
開発環境
Windows11 PC。WSL2でUbuntu22.04LTS。ここでAmazon Q Developer CLIを動作させる。
モデルはClaude Sonnet 4に変更。
実装
今回はオセロリバーシゲームを作る。
ただしルールを変更した。赤い駒(石)をランダムに置いてプレーヤーの邪魔をする。
リバーシに運の要素を入れてみた。
思い付きのローカルルールに対応してくれるだろうか。
早速、Amazon Qにプロンプトを与える。
プロンプト
あなたは斬新なアイディアを持つゲームクリエイターです。これからリバーシのウェブアプリを作りますがルールを変更します。変更ルール1:ゲーム開始から3手目以降から、盤上の空きマスに赤駒が出現したり消えたりします。赤駒の場所、個数、増減はランダムです。赤駒は壁と同じ効果です。赤駒が存在するマスに白駒・黒駒は置けません。終局まで赤駒が残っても構いません。変更ルールは以上で、これ以外は標準のリバーシルールを適用します。実装においてはAWS上のウェブアプリとしてスマートフォンのブラウザで遊べる事を想定してください。上記のゲームをJavascriptのみで実装してウェブアプリとしてください。他のパッケージを使っても構いません。最後に置いた白駒または黒駒のマスの背景を紫色にして、他のマスと区別出来るようにします。ゲームは3モードから選択でき、人間対人間、人間対コンピューター、コンピューター対コンピューター(デモモード)です。デモモードでは、0.5秒以内に手を進めてください。ゲーム名は「ジャミングリバーシ」とします。では、実装してください。
以外に思考に時間がかかっていた。1分ぐらい。
回答
「ジャミングリバーシ」の実装を作成します。赤駒がランダムに出現・消失する斬新なリバーシゲームですね!
+ 1: <!DOCTYPE html>
+ 2: <html lang="ja">
+ 3: <head>
+ 4: <meta charset="UTF-8">
+ 5: <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ 6: <title>ジャミングリバーシ</title>
+ 7: <style>
+ 8: body {
+ 9: font-family: Arial, sans-serif;
+ 10: margin: 0;
+ 11: padding: 20px;
+ 12: background-color: #2c3e50;
+ 13: color: white;
+ 14: text-align: center;
+ 15: }
+ 16:
+ 17: .container {
+ 18: max-width: 600px;
+ 19: margin: 0 auto;
+ 20: }
+ 21:
+ 22: h1 {
+ 23: color: #e74c3c;
+ 24: margin-bottom: 30px;
+ 25: }
+ 26:
+ 27: .game-mode-selector {
+ 28: margin-bottom: 20px;
+ 29: }
+ 30:
+ 31: .mode-button {
+ 32: background-color: #3498db;
+ 33: color: white;
+ 34: border: none;
+ 35: padding: 10px 20px;
+ 36: margin: 5px;
+ 37: border-radius: 5px;
+ 38: cursor: pointer;
+ 39: font-size: 14px;
+ 40: }
+ 41:
+ 42: .mode-button:hover {
+ 43: background-color: #2980b9;
+ 44: }
+ 45:
+ 46: .mode-button.active {
+ 47: background-color: #e74c3c;
+ 48: }
+ 49:
+ 50: .game-info {
+ 51: display: flex;
+ 52: justify-content: space-between;
+ 53: align-items: center;
+ 54: margin-bottom: 20px;
+ 55: flex-wrap: wrap;
+ 56: }
+ 57:
+ 58: .score {
+ 59: font-size: 18px;
+ 60: font-weight: bold;
+ 61: }
+ 62:
+ 63: .current-player {
+ 64: font-size: 16px;
+ 65: padding: 5px 10px;
+ 66: border-radius: 5px;
+ 67: background-color: #34495e;
+ 68: }
+ 69:
+ 70: .board {
+ 71: display: grid;
+ 72: grid-template-columns: repeat(8, 1fr);
+ 73: gap: 2px;
+ 74: background-color: #27ae60;
+ 75: padding: 10px;
+ 76: border-radius: 10px;
+ 77: margin: 0 auto;
+ 78: max-width: 400px;
+ 79: aspect-ratio: 1;
+ 80: }
+ 81:
+ 82: .cell {
+ 83: background-color: #2ecc71;
+ 84: border: 1px solid #27ae60;
+ 85: border-radius: 50%;
+ 86: cursor: pointer;
+ 87: display: flex;
+ 88: align-items: center;
+ 89: justify-content: center;
+ 90: position: relative;
+ 91: aspect-ratio: 1;
+ 92: }
+ 93:
+ 94: .cell:hover {
+ 95: background-color: #26d0ce;
+ 96: }
+ 97:
+ 98: .cell.last-move {
+ 99: background-color: #9b59b6 !important;
+ 100: }
+ 101:
+ 102: .piece {
+ 103: width: 80%;
+ 104: height: 80%;
+ 105: border-radius: 50%;
+ 106: border: 2px solid #333;
+ 107: }
+ 108:
+ 109: .piece.black {
+ 110: background-color: #2c3e50;
+ 111: }
+ 112:
+ 113: .piece.white {
+ 114: background-color: #ecf0f1;
+ 115: }
+ 116:
+ 117: .piece.red {
+ 118: background-color: #e74c3c;
+ 119: border-color: #c0392b;
+ 120: }
+ 121:
+ 122: .controls {
+ 123: margin-top: 20px;
+ 124: }
+ 125:
+ 126: .control-button {
+ 127: background-color: #95a5a6;
+ 128: color: white;
+ 129: border: none;
+ 130: padding: 10px 20px;
+ 131: margin: 5px;
+ 132: border-radius: 5px;
+ 133: cursor: pointer;
+ 134: }
+ 135:
+ 136: .control-button:hover {
+ 137: background-color: #7f8c8d;
+ 138: }
+ 139:
+ 140: .game-over {
+ 141: background-color: #e74c3c;
+ 142: color: white;
+ 143: padding: 20px;
+ 144: border-radius: 10px;
+ 145: margin-top: 20px;
+ 146: }
+ 147:
+ 148: @media (max-width: 480px) {
+ 149: .game-info {
+ 150: flex-direction: column;
+ 151: gap: 10px;
+ 152: }
+ 153:
+ 154: .board {
+ 155: max-width: 350px;
+ 156: }
+ 157: }
+ 158: </style>
+ 159: </head>
+ 160: <body>
+ 161: <div class="container">
+ 162: <h1>🎮 ジャミングリバーシ 🎮</h1>
+ 163:
+ 164: <div class="game-mode-selector">
+ 165: <button class="mode-button active" data-mode="human-human">人間 vs 人間</button>
+ 166: <button class="mode-button" data-mode="human-computer">人間 vs コンピューター</button>
+ 167: <button class="mode-button" data-mode="computer-computer">デモモード</button>
+ 168: </div>
+ 169:
+ 170: <div class="game-info">
+ 171: <div class="score">
+ 172: <span>⚫ <span id="black-score">2</span></span>
+ 173: <span style="margin-left: 20px;">⚪ <span id="white-score">2</span></span>
+ 174: <span style="margin-left: 20px;">🔴 <span id="red-count">0</span></span>
+ 175: </div>
+ 176: <div class="current-player" id="current-player">
+ 177: 現在のプレイヤー: ⚫
+ 178: </div>
+ 179: </div>
+ 180:
+ 181: <div class="board" id="board"></div>
+ 182:
+ 183: <div class="controls">
+ 184: <button class="control-button" onclick="resetGame()">新しいゲーム</button>
+ 185: <button class="control-button" onclick="togglePause()" id="pause-button" style="display: none;">一時停止</button>
+ 186: </div>
+ 187:
+ 188: <div id="game-over" class="game-over" style="display: none;"></div>
+ 189: </div>
+ 190:
+ 191: <script>
+ 192: class JammingReversi {
+ 193: constructor() {
+ 194: this.board = Array(8).fill().map(() => Array(8).fill(0));
+ 195: this.currentPlayer = 1; // 1: black, -1: white
+ 196: this.moveCount = 0;
+ 197: this.gameMode = 'human-human';
+ 198: this.gameOver = false;
+ 199: this.lastMove = null;
+ 200: this.isPaused = false;
+ 201: this.demoInterval = null;
+ 202:
+ 203: this.initializeBoard();
+ 204: this.setupEventListeners();
+ 205: this.render();
+ 206: }
+ 207:
+ 208: initializeBoard() {
+ 209: // Reset board
+ 210: this.board = Array(8).fill().map(() => Array(8).fill(0));
+ 211:
+ 212: // Initial pieces (standard reversi setup)
+ 213: this.board[3][3] = -1; // white
+ 214: this.board[3][4] = 1; // black
+ 215: this.board[4][3] = 1; // black
+ 216: this.board[4][4] = -1; // white
+ 217:
+ 218: this.currentPlayer = 1;
+ 219: this.moveCount = 0;
+ 220: this.gameOver = false;
+ 221: this.lastMove = null;
+ 222: this.isPaused = false;
+ 223:
+ 224: if (this.demoInterval) {
+ 225: clearInterval(this.demoInterval);
+ 226: this.demoInterval = null;
+ 227: }
+ 228: }
+ 229:
+ 230: setupEventListeners() {
+ 231: // Mode selection
+ 232: document.querySelectorAll('.mode-button').forEach(button => {
+ 233: button.addEventListener('click', (e) => {
+ 234: document.querySelectorAll('.mode-button').forEach(b => b.classList.remove('active'));
+ 235: e.target.classList.add('active');
+ 236: this.gameMode = e.target.dataset.mode;
+ 237: this.resetGame();
+ 238: });
+ 239: });
+ 240:
+ 241: // Board clicks
+ 242: document.getElementById('board').addEventListener('click', (e) => {
+ 243: if (this.gameOver || this.isPaused) return;
+ 244:
+ 245: const cell = e.target.closest('.cell');
+ 246: if (!cell) return;
+ 247:
+ 248: const row = parseInt(cell.dataset.row);
+ 249: const col = parseInt(cell.dataset.col);
+ 250:
+ 251: if (this.gameMode === 'human-human' ||
+ 252: (this.gameMode === 'human-computer' && this.currentPlayer === 1)) {
+ 253: this.makeMove(row, col);
+ 254: }
+ 255: });
+ 256: }
+ 257:
+ 258: render() {
+ 259: const boardElement = document.getElementById('board');
+ 260: boardElement.innerHTML = '';
+ 261:
+ 262: for (let row = 0; row < 8; row++) {
+ 263: for (let col = 0; col < 8; col++) {
+ 264: const cell = document.createElement('div');
+ 265: cell.className = 'cell';
+ 266: cell.dataset.row = row;
+ 267: cell.dataset.col = col;
+ 268:
+ 269: // Highlight last move
+ 270: if (this.lastMove && this.lastMove.row === row && this.lastMove.col === col) {
+ 271: cell.classList.add('last-move');
+ 272: }
+ 273:
+ 274: const value = this.board[row][col];
+ 275: if (value !== 0) {
+ 276: const piece = document.createElement('div');
+ 277: piece.className = 'piece';
+ 278:
+ 279: if (value === 1) piece.classList.add('black');
+ 280: else if (value === -1) piece.classList.add('white');
+ 281: else if (value === 2) piece.classList.add('red');
+ 282:
+ 283: cell.appendChild(piece);
+ 284: }
+ 285:
+ 286: boardElement.appendChild(cell);
+ 287: }
+ 288: }
+ 289:
+ 290: this.updateScore();
+ 291: this.updateCurrentPlayer();
+ 292: }
+ 293:
+ 294: updateScore() {
+ 295: let blackCount = 0, whiteCount = 0, redCount = 0;
+ 296:
+ 297: for (let row = 0; row < 8; row++) {
+ 298: for (let col = 0; col < 8; col++) {
+ 299: if (this.board[row][col] === 1) blackCount++;
+ 300: else if (this.board[row][col] === -1) whiteCount++;
+ 301: else if (this.board[row][col] === 2) redCount++;
+ 302: }
+ 303: }
+ 304:
+ 305: document.getElementById('black-score').textContent = blackCount;
+ 306: document.getElementById('white-score').textContent = whiteCount;
+ 307: document.getElementById('red-count').textContent = redCount;
+ 308: }
+ 309:
+ 310: updateCurrentPlayer() {
+ 311: const playerElement = document.getElementById('current-player');
+ 312: if (this.gameOver) {
+ 313: playerElement.textContent = 'ゲーム終了';
+ 314: } else {
+ 315: playerElement.textContent = `現在のプレイヤー: ${this.currentPlayer === 1 ? '⚫' : '⚪'}`;
+ 316: }
+ 317: }
+ 318:
+ 319: isValidMove(row, col) {
+ 320: if (this.board[row][col] !== 0) return false;
+ 321:
+ 322: const directions = [[-1,-1], [-1,0], [-1,1], [0,-1], [0,1], [1,-1], [1,0], [1,1]];
+ 323:
+ 324: for (let [dr, dc] of directions) {
+ 325: if (this.checkDirection(row, col, dr, dc, this.currentPlayer)) {
+ 326: return true;
+ 327: }
+ 328: }
+ 329:
+ 330: return false;
+ 331: }
+ 332:
+ 333: checkDirection(row, col, dr, dc, player) {
+ 334: let r = row + dr;
+ 335: let c = col + dc;
+ 336: let hasOpponent = false;
+ 337:
+ 338: while (r >= 0 && r < 8 && c >= 0 && c < 8) {
+ 339: const cell = this.board[r][c];
+ 340:
+ 341: if (cell === 0 || cell === 2) break; // Empty or red piece
+ 342: if (cell === player) return hasOpponent;
+ 343:
+ 344: hasOpponent = true;
+ 345: r += dr;
+ 346: c += dc;
+ 347: }
+ 348:
+ 349: return false;
+ 350: }
+ 351:
+ 352: makeMove(row, col) {
+ 353: if (!this.isValidMove(row, col)) return false;
+ 354:
+ 355: this.board[row][col] = this.currentPlayer;
+ 356: this.flipPieces(row, col);
+ 357: this.lastMove = { row, col };
+ 358: this.moveCount++;
+ 359:
+ 360: // Add red pieces after 3rd move
+ 361: if (this.moveCount >= 3) {
+ 362: this.updateRedPieces();
+ 363: }
+ 364:
+ 365: this.currentPlayer *= -1;
+ 366:
+ 367: // Check if current player can move
+ 368: if (!this.hasValidMoves(this.currentPlayer)) {
+ 369: this.currentPlayer *= -1; // Switch back
+ 370: if (!this.hasValidMoves(this.currentPlayer)) {
+ 371: this.endGame();
+ 372: }
+ 373: }
+ 374:
+ 375: this.render();
+ 376:
+ 377: // Handle computer moves
+ 378: if (!this.gameOver) {
+ 379: if (this.gameMode === 'human-computer' && this.currentPlayer === -1) {
+ 380: setTimeout(() => this.makeComputerMove(), 500);
+ 381: } else if (this.gameMode === 'computer-computer') {
+ 382: this.startDemoMode();
+ 383: }
+ 384: }
+ 385:
+ 386: return true;
+ 387: }
+ 388:
+ 389: flipPieces(row, col) {
+ 390: const directions = [[-1,-1], [-1,0], [-1,1], [0,-1], [0,1], [1,-1], [1,0], [1,1]];
+ 391:
+ 392: for (let [dr, dc] of directions) {
+ 393: if (this.checkDirection(row, col, dr, dc, this.currentPlayer)) {
+ 394: let r = row + dr;
+ 395: let c = col + dc;
+ 396:
+ 397: while (r >= 0 && r < 8 && c >= 0 && c < 8 && this.board[r][c] === -this.currentPlayer) {
+ 398: this.board[r][c] = this.currentPlayer;
+ 399: r += dr;
+ 400: c += dc;
+ 401: }
+ 402: }
+ 403: }
+ 404: }
+ 405:
+ 406: updateRedPieces() {
+ 407: // Remove existing red pieces
+ 408: for (let row = 0; row < 8; row++) {
+ 409: for (let col = 0; col < 8; col++) {
+ 410: if (this.board[row][col] === 2) {
+ 411: this.board[row][col] = 0;
+ 412: }
+ 413: }
+ 414: }
+ 415:
+ 416: // Add new red pieces randomly
+ 417: const emptySpaces = [];
+ 418: for (let row = 0; row < 8; row++) {
+ 419: for (let col = 0; col < 8; col++) {
+ 420: if (this.board[row][col] === 0) {
+ 421: emptySpaces.push({ row, col });
+ 422: }
+ 423: }
+ 424: }
+ 425:
+ 426: if (emptySpaces.length > 0) {
+ 427: const redCount = Math.floor(Math.random() * Math.min(4, emptySpaces.length)) + 1;
+ 428:
+ 429: for (let i = 0; i < redCount; i++) {
+ 430: const randomIndex = Math.floor(Math.random() * emptySpaces.length);
+ 431: const { row, col } = emptySpaces.splice(randomIndex, 1)[0];
+ 432: this.board[row][col] = 2;
+ 433: }
+ 434: }
+ 435: }
+ 436:
+ 437: hasValidMoves(player) {
+ 438: for (let row = 0; row < 8; row++) {
+ 439: for (let col = 0; col < 8; col++) {
+ 440: if (this.board[row][col] === 0) {
+ 441: const originalPlayer = this.currentPlayer;
+ 442: this.currentPlayer = player;
+ 443: const valid = this.isValidMove(row, col);
+ 444: this.currentPlayer = originalPlayer;
+ 445: if (valid) return true;
+ 446: }
+ 447: }
+ 448: }
+ 449: return false;
+ 450: }
+ 451:
+ 452: makeComputerMove() {
+ 453: if (this.gameOver || this.isPaused) return;
+ 454:
+ 455: const validMoves = [];
+ 456: for (let row = 0; row < 8; row++) {
+ 457: for (let col = 0; col < 8; col++) {
+ 458: if (this.isValidMove(row, col)) {
+ 459: validMoves.push({ row, col });
+ 460: }
+ 461: }
+ 462: }
+ 463:
+ 464: if (validMoves.length > 0) {
+ 465: const randomMove = validMoves[Math.floor(Math.random() * validMoves.length)];
+ 466: this.makeMove(randomMove.row, randomMove.col);
+ 467: }
+ 468: }
+ 469:
+ 470: startDemoMode() {
+ 471: if (this.demoInterval) clearInterval(this.demoInterval);
+ 472:
+ 473: this.demoInterval = setInterval(() => {
+ 474: if (!this.gameOver && !this.isPaused) {
+ 475: this.makeComputerMove();
+ 476: } else {
+ 477: clearInterval(this.demoInterval);
+ 478: this.demoInterval = null;
+ 479: }
+ 480: }, 500);
+ 481: }
+ 482:
+ 483: endGame() {
+ 484: this.gameOver = true;
+ 485:
+ 486: let blackCount = 0, whiteCount = 0;
+ 487: for (let row = 0; row < 8; row++) {
+ 488: for (let col = 0; col < 8; col++) {
+ 489: if (this.board[row][col] === 1) blackCount++;
+ 490: else if (this.board[row][col] === -1) whiteCount++;
+ 491: }
+ 492: }
+ 493:
+ 494: let winner;
+ 495: if (blackCount > whiteCount) winner = '⚫ 黒の勝利!';
+ 496: else if (whiteCount > blackCount) winner = '⚪ 白の勝利!';
+ 497: else winner = '引き分け!';
+ 498:
+ 499: const gameOverElement = document.getElementById('game-over');
+ 500: gameOverElement.innerHTML = `
+ 501: <h2>ゲーム終了</h2>
+ 502: <p>${winner}</p>
+ 503: <p>⚫ ${blackCount} - ⚪ ${whiteCount}</p>
+ 504: `;
+ 505: gameOverElement.style.display = 'block';
+ 506:
+ 507: if (this.demoInterval) {
+ 508: clearInterval(this.demoInterval);
+ 509: this.demoInterval = null;
+ 510: }
+ 511: }
+ 512:
+ 513: resetGame() {
+ 514: document.getElementById('game-over').style.display = 'none';
+ 515: this.initializeBoard();
+ 516: this.render();
+ 517:
+ 518: if (this.gameMode === 'computer-computer') {
+ 519: setTimeout(() => this.startDemoMode(), 1000);
+ 520: }
+ 521:
+ 522: // Show/hide pause button for demo mode
+ 523: const pauseButton = document.getElementById('pause-button');
+ 524: if (this.gameMode === 'computer-computer') {
+ 525: pauseButton.style.display = 'inline-block';
+ 526: } else {
+ 527: pauseButton.style.display = 'none';
+ 528: }
+ 529: }
+ 530:
+ 531: togglePause() {
+ 532: this.isPaused = !this.isPaused;
+ 533: const pauseButton = document.getElementById('pause-button');
+ 534: pauseButton.textContent = this.isPaused ? '再開' : '一時停止';
+ 535:
+ 536: if (!this.isPaused && this.gameMode === 'computer-computer' && !this.gameOver) {
+ 537: this.startDemoMode();
+ 538: } else if (this.isPaused && this.demoInterval) {
+ 539: clearInterval(this.demoInterval);
+ 540: this.demoInterval = null;
+ 541: }
+ 542: }
+ 543: }
+ 544:
+ 545: // Global functions
+ 546: let game;
+ 547:
+ 548: function resetGame() {
+ 549: game.resetGame();
+ 550: }
+ 551:
+ 552: function togglePause() {
+ 553: game.togglePause();
+ 554: }
+ 555:
+ 556: // Initialize game when page loads
+ 557: window.addEventListener('load', () => {
+ 558: game = new JammingReversi();
+ 559: });
+ 560: </script>
+ 561: </body>
+ 562: </html>
● Completed in 0.0s
「ジャミングリバーシ」のウェブアプリを作成しました!以下の特徴を実装しています:
## 主な機能
1. 3つのゲームモード
• 人間 vs 人間
• 人間 vs コンピューター
• デモモード(コンピューター vs コンピューター、0.5秒間隔)
2. ジャミング機能
• 3手目以降から赤駒がランダムに出現・消失
• 赤駒は壁として機能し、その場所には駒を置けない
• 赤駒の数と位置は完全にランダム
3. 視覚的な工夫
• 最後に置いた駒のマスを紫色でハイライト
• スマートフォン対応のレスポンシブデザイン
• 現在のスコア(黒・白・赤駒の数)を表示
4. ゲーム制御
• 新しいゲームボタン
• デモモード用の一時停止/再開機能
• ゲーム終了時の勝敗表示
## 使用方法
1. ファイルをブラウザで開く
2. ゲームモードを選択
3. 盤面をタップ/クリックして駒を配置
4. 3手目以降は赤駒の出現に注意しながらプレイ
このゲームはAWS上でホスティングすることで、スマートフォンのブラウザからアクセス可能です。S3の静的ウェブサイトホスティン
グ機能を使用すれば簡単にデプロイできます。
赤駒のランダム出現により、従来のリバーシとは全く異なる戦略性が生まれる斬新なゲームになっています!
検証
完成したhtmlを開いてみる。一発でどこまで動くだろうか。
まず、ホーム画面は典型的なリバーシゲームのデザインとして出来上がっている。
駒(石)を置く操作も大丈夫そうだ。
デモモードでの検証
さて、今回はルールを変えたのでデモモードで動作を見てみる。
動作をgifアニメで貼ってみる。
zennではアップロードファイルサイズに3MB制限があるので、動画速度が超高速になってしまったのはご容赦いただきたい。
ゲームの最終結果は上の通り。赤が絶妙な邪魔をしていて笑った。
おわりに
リバーシのルールをだいぶ改造したにもかかわらず、見事に実装してくれた。それも一発で。
Amazon Q developer CLIのすごさを感じた。(LLMとしての実態はClaudeだが)
ゲームそのものは、序盤はもう少し赤い駒(石)が多くても良いかもしれない。
この辺はパラメーター調整を細かくできるように、さらに改造すれば良さそう。
今後はソースコードの考察や修正をしたり、実際にAWS環境にデプロイしてスマホで遊んでみたい。
Discussion