💡

Amazon Q Developer CLIで妙なローカルルールのリバーシゲームを作る

に公開

abstract

Amazon Q Developer CLIを利用してゲームを作成した。
ゲームの内容はリバーシの改造版。
AWSのTシャツをもらう。
AWS公式のキャンペーン記事は以下。
https://aws.amazon.com/jp/blogs/news/build-games-with-amazon-q-cli-and-score-a-t-shirt/

開発環境

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